유리수를 나타내는 Rational 클래스와 operator* 함수를 템플릿으로 만들 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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) { ... } | cs |
유리수를 나타내는 클래스이므로, 혼합형(mixed-mode) 수치 연산이 필요합니다. 따라서 다음의 코드가 컴파일되어야 하지만, 에러가 나는 코드가 있습니다.
1 2 | Rational<int> oneHalf(1, 2); Rational<int> result = oneHalf * 2; // 에러입니다! | cs |
컴파일이 되지 않는다는 사실로 미루어 볼 때, 템플릿 버전의 Rational에는 템플릿이기 전의 버전과 무언가가 다릅니다. 템플릿이 아니라면 호출하려고 하는 함수가 무엇인지를 컴파일러가 알고 있지만(Rational 객체 두 개를 받는 operator* 함수), 지금 경우에는 어떤 함수를 호출하려는지에 대해 컴파일러로서는 아는 바가 전혀 없습니다. 단지, 컴파일러는 operator*라는 이름의 템플릿으로부터 인스턴스화할(다시 말해, 만들어낼) 함수를 결정하기 위해 온갖 계산을 동원할 뿐이랍니다. 컴파일러가 확실히 아는 것은 Rational<T> 타입의 매개변수를 두 개 받아들이는 operator*라는 이름의 함수를 자신이 어떻게든 인스턴스로 만들긴 해야 한다는 점입니다. 그러나 이 인스턴스화를 제대로 하려면 T가 무엇인지를 알아야 하지만, 컴파일러는 스스로 알아낼 방법이 없죠.
T의 정체를 파악하기 위해, 컴파일러는 우선 operator* 호출 시에 넘겨진 인자의 모든 타입을 살핍니다. 지금의 경우에는 Rational<int>(oneHalf의 타입) 및 int(2의 타입)입니다. 컴파일러는 이들을 하나씩 각개 격파해 갑니다.
oneHalf의 경우, operator*의 첫 번째 매개변수는 Rational<T> 타입으로 선언되어 있고, 지금 operator*에 넘겨진 첫 번째 매개변수가 마침 또 Rational<int> 타입이기 때문에, T는 int일 수밖에 없습니다. 하지만 애석하게도 두 번째 매개변수의 경우, operator*의 선언을 보면 두 번째 매개변수가 Rational<T> 타입으로 선언되어 있는데, 지금 operator*에 넘겨진 두 번째 매개변수(2)는 int 타입입니다. 이때 컴파일러는 어떻게 해야되는지 유추하지 못하게 되는데, 그 이유는 템플릿 인자 추론(template argument deduction) 과정에서 암시적 타입 변환이 고려되지 않기 때문입니다. 이런 타입 변환은 함수 호출이 진행될 때 쓰이는 것은 맞지만, 템플릿 인자 추론이 진행되는 동안에는 생성자 호출을 통한 암시적 타입 변환 자체가 고려되지 않습니다.
이처럼 힘든 처지에서 템플릿 인자 추론을 해야 하는 수고로부터 컴파일러를 해방시킬 수 있는 방법이 있습니다. 클래스 템플릿 안에 프렌드 함수를 넣어 두면 함수 템플릿으로서의 성격을 주지 않고 특정한 함수 하나를 나타낼 수 있다는 사실을 이용하는 것입니다. 다시 말해, Rationa<T> 클래스에 대해 operator*를 프렌드 함수로 선언하는 것이 가능하다는 이야기입니다. 클래스 템플릿은 템플릿 인자 추론 과정에 좌우되지 않으므로(템플릿 인자 추론은 함수 템플릿에만 적용되는 과정입니다), T의 정확한 정보는 Rational<T> 클래스가 인스턴스화될 당시에 바로 알 수 있습니다. 그렇기 때문에, 호출 시의 정황에 맞는 operator* 함수를 프렌드로 선언하는 데 별 어려움이 없는 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | template<typename T> class Rational { public: ... friend // operator* 함수를 const Rational operator*(const Rational& lhs, // 선언합니다. const Rational& rhs); }; template<typename T> // operator* 함수를 const Rational<T> operator*(const Rational<T>& lhs, // 정의합니다. const Rational<T>& rhs) { ... } | cs |
이제 혼합형 operator* 호출이 컴파일됩니다. oneHalf 객체가 Rational<int> 타입으로 선언되면 Rational<int> 클래스가 인스턴스로 만들어지고, 이때 그 과정의 일부로서 Rational<int> 타입의 매개변수를 받는 프렌드 함수인 operator*도 자동으로 선언되기 때문입니다. 이전과 달리 지금은 함수가 선언된 것이므로(함수 템플릿이 아니라), 컴파일러는 이 호출문에 대해 암시적 반환 함수(Rational의 비명시 호출 생성자 등)를 적용할 수 있게 되는 것이죠.
몇 가지 사용한 문법에 대해 짚고 넘어가자면, 클래스 템플릿 내부에서는 템플릿의 이름(<> 뗀 것)을 그 템플릿 및 매개변수의 줄임말로 쓸 수 있습니다. 그러니까 Rational<T> 안에서는 Rational이라고만 써도 Rational<T>로 먹힌다는 거죠. 매개변수 타입과 반환 타입에서도 똑같은 의미입니다.
그런데 위 코드는 컴파일은 되지만, 링크는 안 됩니다. 우리가 어떤 함수를 호출하려는지 컴파일러가 알 수 있게 됐으니 컴파일은 됐지만, 안에서 선언만 되어 있지, 거기에서 정의까지 되어 있는 것은 아닙니다. 클래스 외부에 있는 operator* 템플릿에서 함수 정의를 제공하도록 만들고 싶지만, 현재 상태로는 안 됩니다.
가장 간단하게 해결하려면, operator* 함수의 본문을 선언부와 붙이면 됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 | 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()); } }; | cs |
이제 operator* 함수의 혼합형 호출 코드가 이제는 컴파일도 되고, 링크도 되고, 실행도 됩니다. 이번 항목의 방법의 핵심은, 프렌드 함수를 선언하긴 했지만, 클래스의 public 영역이 아닌 부분에 접근하는 것과 프렌드 권한은 아무런 상관이 없다는 것입니다. 모든 인자에 대해 타입 변환이 가능하도록 만들기 위해 비멤버 함수가 필요하고, 호출 시의 상황에 맞는 함수를 자동으로 인스턴스화하기 위해서는 그 비멤버 함수를 클래스 안에 선언해야 합니다. 공교롭게도, 클래스 안에 비멤버 함수를 선언하는 유일한 방법이 '프렌드'였을 뿐입니다.
클래스 안에 정의된 함수는 암시적으로 인라인으로 선언됩니다. 지금의 operator* 같은 프렌드 함수도 예외는 아니죠. 클래스의 바깥에서 정의된 도우미 함수만 호출하는 식으로 operator*를 구현하면 이러한 암시적 인라인 선언의 영향을 최소화할 수도 있습니다. 이른바 "프렌드 함수는 도우미만 호출하게 만들기" 방법입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | template<typename T> class Rational; // 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); } // 함수를 호출하게 만듭니다. ... }; | cs |
대다수의 컴파일러에서 템플릿 정의를 헤더 파일에 전부 넣을 것을 사실상 강제로 강요하다시피 하고 있으니, doMultiply도 헤더 파일 안에 정의해 넣어야 할 것입니다.
1 2 3 4 5 6 7 | 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()); } | cs |
물론 doMultiply는 템플릿으로서 혼합형 곱셈을 지원하지 못하겠지만, 지원할 필요가 없습니다. 이 템플릿을 사용하는 고객은 operator*밖에 없을 텐데, operator*가 이미 혼합형 연산을 지원하고 있으니까요. operator* 함수는 자신이 받아들이는 매개변수가 제대로 곱해지도록 어떤 타입도 Rational 객체로 바꿔 주고, 이렇게 바꾼 Rational 객체 두 개는 doMultiply 템플릿 인스턴스가 실제 곱셈에 써먹습니다.
모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면, 이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의합시다.