[Effective C++] 24. 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자.

C++/Effective C++

2020. 8. 31. 16:54

class Rational {
    public:
     Rational (int numerator = 0, int denominator =1);
     int numerator()const;
     int denominator()const;
    private:
    ...
};

 

위의 유리수를 나타내는 클래스가 기본 연산( *,+,-,/ 등..)을 지원하기 위해서는 멤버 함수/ 비멤버 함수/ 비멤버 프렌드 함수중 어떤 것을 선택하는게 좋은가?

 

먼저, 멤버 함수로 기본 연산을 지원한다고 생각해 보자.

 

class Rational {
 public:
  ...
  
  const Rational operator*(const Rational& rhs)const;
};


Rational oneEnglish(1,8);
Rational oneHalf(1,2);

Rational result = oneHalf * oneEnglish; // OK!

result = result * oneEighth; // OK!

result = oneHalf *2; // OK!
result = 2*oneHalf; // Error!

 

위의 코드에서 'result = 2 * oneHalf' 를 주목하자. 이는 어찌보면 당연한 결과인데, 해당 연산을 풀어쓰면

 

result = oneHalf.operator*(2); // 1번 연산
result = 2.operator*(onehalf); // 2번 연산

 

2에는 클래스가 연관되지 않았기에 operator를 호출할 수 없게된다. 하지만 의아한 점이 그렇다면 첫 번째 연산은 어째서 먹히는 것일까? 분명 operator*연산의 매개변수로는 Rational을 받고 있는데 2를 대입해서 넣어줘도 제대로 동작을 하고 있다.

 

이는 바로 암시적 타입 변환에 의해서 일어나는데, 컴파일러가 함수에 int를 넘겨받았지만 Rational을 요구한다는 것을 알고 int를 Rational 클래스 생성자에 주어 호출하여 Rational로 둔갑시킨 것이다. 마치 다음과 같이 동작을 하게 된다.

 

const Rational temp(2);

result = oneHalf * temp;

물론 생성자에 명시 호출(explicit)를 해줬다면 1번 연산도 2번 연산과 마찬가지로 error가 떴을 것이다.

explicit을 해줌으로서 두 문장의 동작은 일관적으로 수행된다. 하지만, 최초의 목적이 혼합 연산을 하기 위함이었기 때문에 멤버 함수를 통한 연산은 불가능하다.

 

정확한 이유는 암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어 있어야하고 이는, 호출되는 멤버 함수를 갖고 있는 (this가 가리키는) 객체에 해당하는 암시적 매개변수에는 암시적 변환히 먹히지 않는다는 뜻이다.

 

최초의 목적을 달성하기 위한 방법은 operator*를 비멤버 함수로 만들어서, 컴파일러 쪽에서 모든 인자에 대해 암시적 타입 변환을 수행하도록 내버려두는 것이다.

 

const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(),
    lhs.denominator() * rhs.denominator();
}

Rational oneFourth (1,4);
Rational result;

result = oneFourth * 2;
result = 2 * oneFourth; // 가능!

 

이것만은 잊지 말자!

  • 어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함해서)에 대해 타입 변환을 해 줄 필요가 있다면, 그 함수는 비멤버이어야 합니다.