항목 24 : 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자
유리수를 나타내는 클래스를 선언하겠습니다.
1
2
3
4
5
6
7
8
9
10
|
class Rational {
public :
Rational(int numerator = 0, int denominator = 1); // 생성자에 일부러 explicit를 붙이지 않았습니다.
// int에서 Rational로의 암시적 변환을 허용하기 위해서 그런 것이죠.
int numerator() const; // 분자 및 분모에 대한 접근용 함수입니다.
int denominator() const;
private :
...
};
Colored by Color Scripter
|
operator*을 Rational의 멤버 함수로 만들어 보겠습니다.
1
2
3
4
5
6
|
class Rational {
public :
...
const Rational operator*(const Rational& rhs) const;
};
Colored by Color Scripter
|
이렇게 설계해 두면 유리수 곱셈을 아주 쉽게 할 수 있게 됩니다.
1
2
3
4
5
|
Rational oneEighth(1, 8);
Ratioanl oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // 문제 없습니다.
result = result * oneEighth; // 문제 없습니다.
|
여기서 혼합형 수치 연산도 가능했으면 좋겠다는 생각을 하기 시작합니다. 하지만 혼합형 수치 연산을 해 보려고 들었더니, 이게 반쪽짜리 연산이라는 사실을 알게 됩니다.
1
2
|
result = oneHalf * 2; // 이상없음.
result = 2 * oneHalf; // 에러!
|
이 문제의 원인은 위의 두 예제를 함수 형태로 바꾸어 써 보면 바로 드러납니다.
1
2
|
result = oneHalf.operator*(2); // 이상없음.
result = 2.operator*(oneHalf); // 에러!
|
첫 번째 줄에서 oneHalf 객체는 operator* 함수를 멤버로 갖고 있는 클래스의 인스턴스이므로, 컴파일러는 이 함수를 호출합니다. 하지만 두 번째 줄에서 정수 2에는 클래스 같은 것이 연관되어 있지 않기 때문에, operator* 멤버 함수도 있을 리가 없습니다. 컴파일러는 아래처럼 호출할 수 있는 비멤버 버전의 operator*(네임스페이스 혹은 전역 유효범위에 있는 operator*)도 찾아봅니다.
1
|
result = operator*(2, oneHalf); // 에러!
|
그러나 작금의 예제에서는 int와 Rational을 취하는 비멤버 버전의 operator*가 없으므로 탐색은 실패하고 컴파일 에러가 나게 됩니다.
위에서 제대로 성공한 함수 호출문을 다시 들여다봅시다. 두 번째 매개변수가 정수인데, Rational::operator*의 선언문을 보면 인자로 Ratioanl 객체를 받도록 되어 있습니다. 이게 무슨 조화일까요? 2가 어디에선 먹히고 어디에선 안 먹히는 이유가 무엇일까요?
조화는 바로 암시적 타입 변환에 있습니다. 컴파일러는 여러분이 이 함수에 int를 넘겼으며 함수 쪽에선 Rational을 여구한다는 사실을 알고 있으나, 이 int를 Rational 클래스의 생성자에 주어 호출하면 Rational로 둔갑시킬 수 있다는 사실도 알고 있습니다. 그래서 컴파일러는 자기가 알고 있는 대로 한 것입니다. 다시 말해, 마치 아래와 같이 작성된 코드인 것처럼 처리한 거죠.
1
2
3
|
const Rational temp(2); // 2로부터 임시 Rational 객체를 생성합니다.
result = oneHalf * temp; // oneHalf.operator*(temp)와 같습니다.
|
물론 컴파일러가 이렇게 동작한 것은 명시호출로 선언되지 않은 생성자가 있기 때문입니다. Rational 생성자가 만약 명시호출 생성자였으면 다음의 코드 중 어느 쪽도 컴파일되지 않습니다.
1
2
3
|
result = oneHalf * 2; // 에러(명시호출 생성자에 의해) 2를 Rational로 바꿀 수 없습니다.
result = 2 * oneHalf; // 역시 같은 문제입니다.
|
Rational 생성자가 명시호출이 아닐 때도 어째서 첫 번째 문장은 컴파일되는데 두 번째 문장은 안 되는지 고민해 보도록 합시다.
1
2
3
|
result = oneHalf * 2; // 컴파일 됩니다(비명시호출 생성자와 함께)
result = 2 * oneHalf; // 안 됩니다!(비명시호출 생성자와 함께 했는데도)
|
이로써 알 수 있는 사실은, 암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어 있어야만 한다는 것입니다. 그러니까 호출되는 멤버 함수를 갖고 있는(쉽게 말해 this가 가리키는) 객체에 해당하는 암시적 매개변수에는 암시적 변환이 먹히지 않습니다. 첫 번째 문장이 컴파일되고 두 번째 문장이 컴파일되지 않는 이유도 바로 이것입니다. 전자의 경우는 매개변수 리스트에 있는 매개변수가 쓰이고 있지만, 후자의 경우는 그렇지 않죠.
하지만 여전히 혼합형 수치 연산을 지원하고 싶은 생각이 있습니다. 어떻게 해야 할지도 알겠고요. 바로 operator*를 비멤버 함수로 만들어서, 컴파일러 쪽에서 모든 인자에 대해 암시적 타입 변환을 수행하도록 내버려 두는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class Rational {
... // operator*가 없습니다.
};
const Rational operator*(const Rational& lhs, const Ratioanl& rhs) // 이제는 비멤버 함수입니다.
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // 됩니다!
result = 2 * oneFourth; // 이제는 이것도 됩니다! Colored by Color Scripter
|
걱정거리가 하나 남아있습니다. operator* 함수는 Rational 클래스의 프렌드 함수로 두어도 될까요?
지금의 예제에서는 '아니오'라고 답해야 옳습니다. operator*는 완전히 Rational의 public 인터페이스만을 써서 구현할 수 있기 때문입니다.
그러고 보니 여기서 한 가지 중요한 결론을 뽑을 수 있게 되었습니다. "멤버 함수의 반대는 프렌드 함수가 아니라 비멤버 함수이다"라는 것입니다.
꼭 잊지 말아야 할 것!
어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함해서)에 대해 타입 변환을 해 줄 필요가 있다면, 그 함수는 비멤버이어야 합니다.
'언어 > C++' 카테고리의 다른 글
[Effective C++] 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2020.01.26 |
---|---|
[Effective C++] 예외를 던지지 않는 swap에 대한 지원도 생각해 보자 (0) | 2020.01.25 |
[Effective C++] 멤버 함수보다는 비멤버 비프렌드 함수와 더 까가워지자 (0) | 2020.01.23 |
[Effective C++] 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2020.01.22 |
[Effective C++] 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자 (0) | 2020.01.21 |