클래스에서 암시적 타입 변환을 지원하는 것은 일반적으로 못된 생각이다. 그러나 예외가 있는데, 가장 흔한 예외 중

하나가 숫자 타입을 만들 때이다. 예를 들어 유리수를 나타내는 클래스를 만들고 있다면, 정수에서 유리수로의 암시적

변환은 허용하더라도 잘못되었다고 볼 수 없다. 즉, int를 double 형으로 변환하는 것과 동일하다고 할 수 있다.

 

1
2
3
4
5
6
7
8
class Rational
{
public:
    Rational(int numerator = 0int denominator = 1); //암시적 변환을 허용하기 위해서 explicit을 붙이지 않았다.
 
    int numerator() const;        //분자.
    int denominator() const;    //분모.
};
cs

 

유리수를 나타내는 클래스인만큼 덧셈이나 곱셉 등의 수치 연산은 기본으로 지원하게 만들고 싶을텐데, 이럴 때는 어떤 식으로 지원해야

좋을지 고민해봐야 한다.

아마 흔히 생각나는 방법은 멤버 함수로 operator 연산자를 이용하여 구현하는 방법일 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Rational
{
public:
    ...
 
    const Rational operator* (const Rational& rhs) const;
};
 
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneEight * oneHalf;    //이상 없음.
 
    result = result * oneEight;              //이상 없음.
}
cs

 

여기서 혼합형 수치 연산도 가능하길 원한다면 (Rational 객체를 int 같은 것과도 곱할수 있도록), 이것은 반쪽짜리 연산이었다는

사실을 알게 될 것이다. 하기 소스를 보자.

 

1
2
3
4
5
6
7
8
9
10
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;   //이상 없음.
 
    result = 2 * oneHalf;            //컴파일 에러.
}
 
cs

 

곱셈은 기본적으로 교환법칙이 성립해야 되는데 상기에 있는 소스는 컴파일 에러가 발생한다.
상기 소스를 함수 형태로 바꾸어 써 보면 왜 컴파일 에러가 발생하는지 알 수 있다. 하기 소스 참조.

 

1
2
3
4
5
6
7
8
9
10
11
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;    
    // => Rational result = oneHalf.operator*(2);
 
    result = 2 * oneHalf;
    // => result = 2.operator*(oneHalf);
}
cs

 

첫 번째 result 연산에서 oneHalf 객체는 operator* 연산자를 멤버로 갖고 있는 클래스의 인스턴스이므로,
컴파일러는 이 함수를 문제없이 호출한다. 하지만 두 번째 result 연산에서 정수 2에는 클래스 같은 것이 아니기 때문에,

operator* 연산자가 있을 리가 없다. 그럼 컴파일러는 호출할 수 있는 비멤버 operator*(네임스페이스 또는 전역 유효범위에

있는 operator*) 연산자를 찾아본다.

 

result = operator*(2, oneHalf); // 컴파일 에러.

 

그러나 상기 소스와 같이 int형과 Rational을 인자로 갖는 비멤버 버전의 operator* 연산자가 없으므로 컴파일 에러가 발생한다.

 

그럼, 상기 소스에서 Rational result = oneHalf.operator*(2);는 문제가 발생하지 않은 이유에 대해서 알아보자.
이유는 바로 암시적 타입 변환이다.

컴파일러는 함수 쪽에선 Rational 타입을 요구하지만 프로그래머가 operator* 함수에 int를 넘겼다는 사실을 알고 있으나,

이 int를 Rational 클래스의 생성자에 주어 호출하면 Ratinal로 둔갑시킬 수 있다는 사실 또한 알고 있다.

즉, 컴파일러는 하기와 같은 소스로 처리한다는 것이다.

 

1
2
3
4
5
6
    Rational result = oneHalf * 2;    
    // => Rational result = oneHalf.operator*(2);
 
    //result = oneHalf * 2; 상기 문장을 컴파일러는 하기와 같이 처리한다.
    const Rational temp(2);            //2로부터 임시 Rational 객체를 생성.
    Rational result = oneHalf * temp; // oneHalf.operator*(temp); 와 같은 문장.
cs

 

물론 컴파일러가 이렇게 동작하는 것은 명시호출(explicit)로 선언되지 않은 생성자가 있기 때문이다.

만약 Rational 생성자가 명시호출 생성자였으면 하기 코드 모두 컴파일 에러가 발생한다.

 

1
2
3
    Rational result = oneHalf * 2;   //컴파일 에러.
 
    result = 2 * oneHalf;            //컴파일 에러.
cs

 

 

그럼 이제 혼합형 수치 연산이 문제 없도록 만들어보자.

우선 여태까지 알아봤던 결론으로는 암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어 있어야만 한다는

것이다. 그러니까 멤버 함수를 호출하는 객체에 해당하는 암시적 매개변수는 암시적 변환이 먹히지 않는다는 것이다.

(즉, 2 가 operator* 멤버 함수를 호출하는 객체이므로 이렇게 멤버 함수를 호출하는 쪽의 객체는 암시적 형변환이 되지 않는다는

 것이다.)

 

결국 이를 해결하기 위해서는 2가 멤버 함수를 호출하지 않는 방향으로 가야하므로 operator* 함수를 멤버 함수가 아닌 비멤버 함수로

선언하여 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Rational
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    Rational(int numerator = 0int denominator = 1); //암시적 변환을 허용하기 위해서 explicit을 붙이지 않았다.
 
    int numerator() const;        //분자.
    int denominator() const;      //분모.
};
 
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational((lhs.numerator() * rhs.numerator()), (lhs.denominator() * rhs.denominator()));
}
 
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;
    // => Rational result = operator*(oneHalf, temp(2));  정상 동작.
 
    result = 2 * oneHalf;
    // => result = operator*(temp(2), oneHalf);           정상 동작.
}
cs

 

여기서 한 가지 더 고민해볼 것이 있다. Operator* 함수를 Rational 클래스의 프랜드 함수로 두어도 되는가?에 대한 질문이다.

그리고 이 예제에서의 대답은 '아니오'이다.

왜냐하면 이 예제에서 operator* 연산자는 완전히 Rational의 public 인터페이스만을 써서 구현할 수 있기 때문이다.
여기서 한 가지 중요한 결론을 뽑을 수 있는데.. 멤버 함수로 하면 안되는 함수에 대해서 프랜드로 만든다고 해결되는 것이 아니라는 점이다.
즉, "멤버 함수이면 안 되니까"라는 말이 반드시 "프랜드 함수이어야 해"라는 말이 아니라는 것이다.
멤버 함수의 반대는 프랜드 함수가 아니라 비멤버 함수이다.

 

1줄 요약

 

 - 어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함)에 대해 암시적 타입 변환을 해 줄 필요가 있다면,

   그 함수는 비멤버 함수이어야 한다.

+ Recent posts