스토리텔링 개발자

[Effective C++] 46. 템플릿 클래스 안에 비멤버 함수 두기 본문

개발/Effective C++

[Effective C++] 46. 템플릿 클래스 안에 비멤버 함수 두기

김디트 2024. 7. 18. 11:23
728x90

항목 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자

 

 

 

  • 선행 사항 : 우선 '모든 매개변수에 대해 암시적 타입 변환을 지원하려면 비멤버 함수밖에 방법이 없다'를 숙지하는 것이 좋다.(항목 24 참조)

 

 

 

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;
    ...
};

// operator* 템플릿 비멤버 함수
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{ ... }


Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // 컴파일 에러!!!!
  • 문제점
    • 비템플릿 버전의 경우 어떤 함수를 호출하려는지(Rational 객체 두개를 받는 operator*) 컴파일러가 명확히 안다.
    • 하지만, 템플릿 버전의 경우 어떤 함수를 호출하려는지 컴파일러로서는 아는 바가 전혀 없다.
  • 컴파일러는 Rational<T> 타입의 매개변수를 두 개 받는 operator* 함수로 어떻게든 인스턴스를 만들려 한다.
  • 하지만, 이 인스턴스화를 진행하기 위해선 T 타입이 뭔지 추론해야 하나, 컴파일러는 이를 추론할 수 없다.

 

 

 

operator*에 우겨넣기 위한 컴파일러의 추론 과정
  • T의 정체를 파악하기 위해 우선 operator* 호출 시에 넘겨진 인자의 모든 타입을 살핀다.
    • 지금의 경우 Rational<int>(oneHalf의 타입)와 int(2의 타입) 두 종류이다.
  • oneHalf의 경우
    • Rational<T> 타입과 Rational<int> 타입을 대응해 내는 데 성공한다.
  • 2의 경우
    • operator*의 두 번째 매개변수가 Rational<T> 타입이며, 그러니 2는 Rational<int>로 암시적으로 변환될 것이다?
    • 하지만 컴파일러는 이렇게 추론해내지 못한다.
  • 템플릿 인자 추론 과정에서는 암시적 타입 변환이 고려되지 않으므로 Rational<T> 매개변수에 2를 대입하는 것에 실패한다!

 

 

 

해결 방법
  • 클래스 템플릿 안에 friend 함수를 선언하여 해결한다.
    • 이렇게 하면 함수 템플릿으로서의 성격을 주지 않고 특정한 함수 하나를 나타낼 수 있기 때문이다.
    • 즉, 이 friend 함수는 템플릿 인자 추론 과정을 회피할 수 있다.
template<typename T>
class Rational
{
public:
    ...
    // 템플릿 클래스 내에 friend 함수 선언
    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<T> 안에서는 Rational이라고만 써도 Rational<T>로 먹힌다.
  • 허나 이 코드의 경우 링크 에러가 발생한다.

 

 

 

링크 에러 해결
  • friend 함수의 정의가 없기 때문이다.
    • 클래스 외부의 operator* 템플릿이 함수 정의를 제공했으면 했지만... 당연히 자동으로 해결되진 않았다.
  • 가장 간단한 해결 방법은, 함수 본문을 선언부와 붙이는 것이다.
template<typename T>
class Rational
{
public:
    ...
    friend const Rational operator*(const Rational& lhs, const Rational& rhs)
    {
        return Rational(lhs.numerator() * rhs.numerator(),
                        rhs.denominator() * rhs.denominator());
    }
};
  • 클래스 안에 정의된 함수는 인라인으로 선언된다.
    • 클래스 바깥에서 정의된 도우미 함수만 호출하는 식으로 operator*를 구현하면 위의 암시적 인라인 선언의 영향을 최소화할 수 있을 것이다.
    • "프랜드 함수는 도우미만 호출하게 만들기" 방법
  • 도우미 함수를 사용한 대략적인 예제
template<typename T> class Rational;

// 도우미 함수 템플릿
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());
}

template<typename T>
class Rational
{
public:
    ...
    friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
    {
        // 프렌드 함수가 도우미 함수를 호출하게 만든다.
        return doMultiply(lhs, rhs);
    }
    ...
};

// 혼합형 곱셈 지원을 위한 템플릿 비멤버 함수
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{ ... }
  • 이 경우, doMultiply 함수 템플릿은 혼합형 곱셈을 지원하지 못할테지만, 어차피  operator*가 지원해주니 문제 없다.

 

 

 

friend 함수의 용법
  • 보통은 friend 함수를, 제한 영역(protected, private 영역)에 접근하기 위해 사용한다.
  • 하지만 이 경우에는 모든 인자에 대해 타입 변환이 가능하도록 만들기 위해서 friend 함수를 선언했다.
    • 공교롭게도 클래스 안에 비멤버 함수를 선언하는 유일한 방법이 friend 였을 뿐이다.
728x90
Comments