46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
항목 24에서 모든 매개변수에 대해 암시적 타입 변환이 되도록 만들기 위해서는 비멤버 함수밖에 방법이 없다고 했었다. 이번엔 템플릿의 영역에서 알아보자.
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {...}
Rational<int> onHalf(1, 2);
Rational<int> result = onHalf * 2; // 컴파일 에러!
위의 코드는 컴파일되지 않는다. 항목 24에서는 우리가 호출하려고 하는 함수가 무엇인지를 컴파일러가 알고 있지만, 지금 경우에는 어떤 함수를 호출하려는지에 대해 컴파일러로서는 아는 바가 전혀 없다. 이 시점에서 컴파일러가 확실히 아는 것은 Rational<T> 타입의 매개변수를 두 개 받아들이는 operator*라는 이름의 함수를 자신이 어떻게든 인스턴스로 만들긴 해야 한다는 점이다.
T의 정체를 파악하기 위해, 컴파일러는 우선 opeartor* 호출 시에 넘겨진 인자의 모든 타입을 살핀다. 첫 번째 매개변수는 Rational<T> 타입으로 선언되어 있고 Rational<int> 타입이기에 T는 int일 수 밖에 없다. 그러나 두 번째 매개변수는 유추해내기가 그리 녹록치 않다. 두 번째 매개변수는 int 타입인데 이를 통해 T가 int라고 유추할 수 있지 않을까 하고 예상할 것 같지만, 컴파일러는 그렇게 동작하지 못한다. 그 이유는, 템플릿 인자 추론(template argument deduction) 과정에서는 암시적 타입 변환이 고려되지 않기 때문이다. 템플릿 인자 추론이 진행되는 동안에는 생성자 호출을 통한 암시적 타입 변환 자체가 고려되지 않는다.
클래스 템플릿 안에 프렌드 함수를 넣어 두면 함수 템플릿으로서의 성격을 주지 않고 특정한 함수 하나를 나타낼 수 있다는 사실을 이용하자. 클래스 템플릿은 템플릿 인자 추론 과정에 좌우되지 않으므로, T의 정확한 정보는 Rational<T> 클래스가 인스턴스화될 당시에 바로 알 수 있다. 그렇기 때문에, 호출 시의 정황에 맞는 opeartor* 함수를 프렌드로 선언하는 데 별 어려움이 없다.
template<typename T>
class Rational {
public:
...
friend const Rational operator* (const Rational& lhs, const Rational& rhs);
};
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs) {}
이전과 달리 지금은 함수가 선언된 것이므로(함수 템플릿이 아니라), 컴파일러는 이 호출문에 대해 암시적 변환 함수를 적용할 수 있게 되는 것이다. 하지만 이 코드는 컴파일은 되지만 링크가 안 된다. Rational 안에서 선언만 되어 있지, 거기에서 정의까지 되어 있는 것은 아니다.
가장 간단히 해결하려면, opeartor* 함수의 본문을 선언부와 붙이면 된다.
template<typename T>
class Rational {
public:
...
friend const Rational operator* (const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
};
이제 컴파일도 되고, 링크도 되고, 실행도 된다.
프렌드 함수를 선언하긴 했지만, 클래스의 public 영역이 아닌 부분에 접근하는 것과 프렌드 함수는 아무런 상관이 없다. 모든 인자에 대해 타입 변환이 가능하도록 만들기 위해 비멤버 함수가 필요하고(항목 24), 호출 시의 상황에 맞는 함수를 자동으로 인스턴스화하기 위해서는 그 비멤버 함수를 클래스 안에 선언해야 한다. 공교롭게도, 클래스 안에 비멤버 함수를 선언하는 유일한 방법이 프렌드였을 뿐이다.
위의 함수는 암시적으로 인라인 함수이다. 클래스의 바깥에서 정의된 도우미 함수만 호출하는 식으로 operator*를 구현하면 이러한 암시적 인라인 선언의 영향을 최소화할 수 있다. 지금은 코드가 짧아 상관없지만, 꽤 복잡하게 작성된 함수라면 한 번 해 봄 직하다. 이른바 "프렌드 함수는 도우미만 호출하게 만들기"이다.
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);
template<typename T>
class Rational {
public:
...
friend const Rational operator* (const Rational& lhs, const Rational& rhs)
{
return doMultiply(lhs, rhs);
}
};
이것만은 잊지 말자!
- 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면, 이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의하자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 48: 템플릿 메타프로그래밍, 하지 않겠는가? (0) | 2023.11.18 |
---|---|
[Effective C++] 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 (0) | 2023.11.18 |
[Effective C++] 45: "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! (0) | 2023.11.15 |
[Effective C++] 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (1) | 2023.11.13 |
[Effective C++] 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2023.11.13 |