- 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면, 이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의해라.
비멤버 함수를 클래스 템플릿 안에
모든 매개변수에 대해 암시적 타입 변환이 되도록 만들기 위해서는 비멤버 함수로 만들어야 한다.
Rational 클래스와 operator*함수를 템플릿으로 만들 것이다.
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, cosnt Rational<T>& rhs)
{ ... }
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // Error
에러가 발생하는 부분은 어떤 함수를 호출하려는지에 대해 컴파일러가 알 수 없기 때문이다.
컴파일러는 Rational<T>타입의 매개변수를 두 개 받아들이는 operator*라는 이름의 함수를 인스턴스로 만드려 할 것이다.
인스턴스화를 하려면 T가 무엇인지에 대해 알아야 한다.
하지만, 컴파일러는 T가 무엇인지 알 수 없다.
T가 무엇인지 알기 위해, 컴파일러는 우선 operator* 호출 시에 넘겨진 인자의 모든 타입을 살핀다.
지금의 경우에는 Rational<int>(oneHalf)와 int(2)이다.
operator*의 첫 번째 매개변수는 Rational<T>타입으로 선언되어 있고, 지금 operator*에 넘겨진 첫 번째 매개변수는 Rational<int> 타입이기 때문에, T는 int이다.
하지만, 두 번째 매개변수(2)는 유추해내기 쉽지 않다.
operator*의 선언을 보면 두 번째 매개변수가 Rational<T>타입으로 선언되어 있는데, 지금 operator*에 넘겨진 두 번째 매개변수는 int타입이다.
Rational<int>에는 explicit로 선언되지 않은 생성자가 있음에도 컴파일러가 이 생성자를 써서 2라는 매개변수를 Rational<int>로 변환하고 이를 통해 T가 int라고 유추할 수는 없다.
템플릿 인자 추론 과정에서 암시적 타입 변환이 고려되지 않기 때문이다.
타입 변환은 함수 호출이 진행될 때 쓰이는 것은 맞지만 함수를 호출할 수 있으려면 어떤 함수가 있는지를 알고 있어야 한다. 게다가 호출하는 상황에 맞는 함수 템플릿에 넣어 줄 매개변수 타입을 추론하는 일도 해야 한다.
템플릿 인자 추론이 진행되는 동안에는 생성자 호출을 통한 암시적 타입 변환 자체가 고려되지 않는다.
템플릿 인자 추론을 해결하는 방법이 있다.
클래스 템플릿 안에 프렌드 함수를 넣어 두면 함수 템플릿으로서의 성격을 주지 않고 특정한 함수 하나를 나타낼 수 있다는 사실을 이용하는 것이다.
즉, Rational<T>클래스에 대해 operator*를 프렌드 함수로 선언하는 것이 가능하다는 이야기이다.
템플릿 인자 추론과정은 함수 템플릿에만 적용되고 클래스 템플릿과는 관련 없으므로, T의 정확한 정보는 Rational<T> 클래스가 인스턴스화될 당시에 바로 알 수 있다.
그렇기 때문에, 호출 시의 상황에 맞는 operator*함수를 프렌드로 선언하는 데 별 어려움이 없는 것이다.
template<typename T>
class Rational {
public:
...
friend cosnt Rational oerator*(const Rational& lhs, const Rational& rhs);
};
template<typename T>
const Rational<T> operator*(const Rational& lhs, const Rational& rhs)
{ ... }
이제 혼합형 operator*호출이 컴파일된다.
Rational<int> 타입으로 선언되면 Rational<int> 클래스가 인스턴스로 만들어지고, 이때 그 과정의 일부로서 Rational<int> 타입의 매개변수를 받는 프렌드 함수인 operator*도 자동으로 선언되기 때문이다.
지금은 함수 템플릿이 아닌 함수가 선언된 것이므로, 컴파일러는 이 호출문에 대해 암시적 변환 함수를 적용할 수 있게 되는 것이다.
클래스 템플릿 내부에서는 템플릿의 이름을 그 템플릿 및 매개변수의 줄임말로 쓸 수 있다.
즉, Rational<T> 안에서는 Rational이라고 써도 Rational<T>로 해석된다. 매개변수가 여러 개이거나 매개변수 이름이 길 경우에는 효과적이다.
위의 예제에서 operator*의 반환 타입이 Rational로 되어 있다.
컴파일은 성공했지만, 링크가 되지 않는다.
그런데 이 함수는 Rational안에서 선언만 되어 있고 정의가 되어 있지 않다.
클래스 외부에 있는 operator* 템플릿에서 함수 정의를 제공하도록 만들고 싶은 것이 의도였다
그런데 정의를 링커가 찾지 못해서 에러가 발생한 것이었다.
이 문제를 해결하려면, operator* 함수의 본문을 선언부와 붙이면 된다.
template<typename T>
class Rational {
public:
...
friend const Rational operator*(const Rational& lhs, const Ratinal& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
};
프렌드 함수를 선언하긴 했지만, 클래스의 private 영역에 접근하는 것과 프렌드 권한은 아무런 상관이 없다.
모든 인자에 대해 타입 변환이 가능하도록 만들기 위해 비멤버 함수가 필요하고, 호출 시의 상황에 맞는 함수를 자동으로 인스턴스화하기 위해서는 그 비멤버 함수를 클래스 안에 선언해야 한다.
클래스 안에 비멤버 함수를 선언하는 유일한 방법이 프렌드 함수인 것이다.
클래스 안에 정의된 함수는 암시적으로 인라인으로 선언된다.
operator* 같은 프렌드 함수도 예외가 아니다.
클래스의 바깥에서 정의된 함수만 호출하는 식으로 operator*를 구현하면 이러한 암시적 인라인 선언의 영향을 최소화할 수 있다.
이번 예제에서는 이미 한 줄로 되어 있기 때문에 효과가 적을 수 있다.
복잡한 함수 본문이 있다면 적용하는게 좋을 것이다.
Rational이 템플릿이니 외부 함수도 템플릿일 수 있다.
template<typename T>
class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, cosnt Rational<T>& rhs);
template<typename T>
class Rational {
public:
...
friend Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{ return doMultiply(lhs, rhs); }
...
};
대다수의 컴파일러에서 템플릿 정의를 헤더 파일에 전부 넣을 것을 권장한다.
따라서, doMultiply도 헤더 파일 안에 정의해 넣어야 할 것이다.
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
doMultiply는 템플릿으로서 혼합형 곱셈을 지원하지 못하겠지만, 지원할 필요가 없다.
이 템플릿 사용잔느 operator*밖에 사용할 수 없고 operator*가 이미 혼합형 연산을 지원하고 있기 때문이다.
operator* 함수는 자신이 받아들이는 매개변수가 제대로 곱해지도록 어떤 타입도 Rational 객체로 바꿔 주고, 이렇게 바꾼 Rational 객체 두 개는 doMultiply 템플릿의 인스턴스가 받아 실제 곱셈에 사용한다.