본문 바로가기

책 스터디

[Exceptional C++ Style] 8. 템플릿 친구 만들기 (friend function template)

책에서는 friend 함수 템플릿 특수화를 사용하는 케이스에 대해 범위 지정 연산자 (::)를 사용할 수 있는 경우에 대해,
주로 예시를 들면서 범위 지정 연산자가 있을 때 사용할 수 있는 두 가지 방법에 대해 논의한다. 

하지만 실제 범위 지정 연산자를 쓸 수 없는 경우(클래스 정적 멤버가 아니거나 네임스페이스 속에 있지 않거나)도 많기에
이번 챕터 정리는 책과 조금 다르게 풀어갈 생각이며, (범위 지정 연산자를 사용하는 경우를 예외적으로 소개하겠다)
책에서 얘기하고 있는 friend 함수의 조건도 바뀐 C++ 0X 기준으로 설명하겠다. 

주어진 객체에 대해 로그를 남긴 후 삭제하는 다음 함수 템플릿이 있다고 하자.
template<typename T>
void LoggedDelete(T* t)
{
    // 로그를 남겨라
    delete t;
}
그런데, 이 함수 템플릿을 이용하여 지울려는 객체의 클래스가 소멸자를 private로 숨겨 놨다.
class Test
{
    //...
private:
    ~Test() {}    // PRIVATE !!!
}

Test* t = new Test;
LoggedDelete(t);    // C2248 : Test::~Test : private 멤버에 엑세스 할 수 없다.
이 상황을 타개하려면, 
1) Test 클래스의 소멸자를 public으로 돌리는 방법이 있고, 
2) LoggedDelete() 함수를 friend 함수로 두는 방법이 있을 수 있겠다.

[C++ 03]에 따르면, 함수 템플릿을 friend 함수로 선언하는 방법은 크게 두 가지 방법이 있으며,
아래 규칙들은 넘버링순으로 우선 순위를 가진다.

1. friend 함수명이 템플릿 특수화의 형태를 가지면, 특수화된 함수 템플릿은 friend 함수가 될 수 있다.
    : ex) Name<>( ... ) 또는 Name<Test>(Test* t) 등
    : 결국 꺽쇠(<>)를 가지는 형태

2. 만약 템플릿 꺽쇠 형태가 아닌, 범위 지정 연산자(::)가 붙어 있는 형태라면...
    : ex) boost::checked_delete( ... )

2-1. 그 범위에 똑같은 시그너쳐를 가지는 일반 함수가 있으면, 그 일반 함수가 friend 함수가 된다.      
2-2. 그 범위에 똑같은 시그너처를 가지는 함수 템플릿이 존재하면, 그 함수 템플릿 특수화가 friend 함수가 된다.

즉, 범위 내 같은 시그너쳐가 여러 개 있을 때 일반 함수가 우선권을 가진다.

3. 위 모든 상황이 아니라면, 일반 함수만 friend 함수가 될 수 있다.

위 규칙에서도 알 수 있듯이 함수 템플릿을 friend 함수로 만드는 데 컴파일러가 가장 먼저 체크하는 것이
friend 함수를 선언할 때 template <>의 형태로 작성을 하였는가 이다.
표준에서 가장 선호하고 장려하는 방식임을 알 수 있다.
template <typename T>
void LoggedDelete(T* t)
{
    // 로그를 남겨라
    delete t;
}

class Test
{
    //...
private:
    ~Test() {}    // PRIVATE !!!

    friend void LoggedDelete<>(Test* t);       // <>
    friend void LoggedDelete<Test>(Test* t);   // <name>
};

2번 규칙에 의거하여, 범위 지정 연산자를 사용할 수 있는 함수 템플릿을 <> 없이 아래와 같이 friend 함수 선언이 가능하다.
namespace DEL
{
    template <typename T>
    void LoggedDelete(T* t)
    {
        // 로그를 남겨라
        delete t;
    }
}

class Test
{
    //...
private:
    ~Test() {}    // PRIVATE !!!

    friend void DEL::LoggedDelete(Test* t);    // :: 를 이용하여 template <> 없이 선언함.
};
하지만, 이 경우 범위 내 시그너쳐가 같은 일반 함수가 있을 경우, 또는 나중에 추가라도 되는 경우에는
2-2 규칙보다는 2-1 규칙이 우선시 되기에, 함수 템플릿 특수화가 아닌 일반 함수가 friend 함수가 되어 버린다.
이런 측면은 상당히 미묘해서 실수하기가 쉬우니, 이에 의존하는 것은 바람직하지 않다.

또한, 모든 함수 템플릿이 네임스페이스 속에 있거나 클래스 종속적이진 않을 것이다.
심지어, 존재하던 네임스페이스가 추후 누군가에 의해 사라질 수도 있는 것이다.
예를 들어, 다음과 같이 꺽쇠도 범위 지정 연산자도 사용하지 않고, friend 함수로 선언하는 경우는 어떻게 될까?
template <typename T>
void LoggedDelete(T* t)
{
    // 로그를 남겨라
    delete t;
}

class Test
{
    //...
private:
    ~Test() {}    // PRIVATE !!!

    friend void LoggedDelete(Test* t);    // template<>도 없고, :: 도 없이 그냥 선언함
};
이 경우 규칙 3번에 해당되어 일반 함수만 friend 함수가 될 수 있고, 
LoggedDelete는 함수 템플릿이기에 컴파일 에러가 발생한다. (link 에러가 나겠지)

이처럼 범위 지정 연산자를 쓸 수 있는 경우 이제 의지하여 template <> 없이 사용한다면, 
원하지 않는 결과가 발생할 수도 있으며, 일관성 있는 습관을 가지기도 어려워진다.
따라서, 함수 템플릿을 friend로 선언할 때는 <>를 사용하도록 습관화하는 것이 중요하다.
 
다시 한 번 아래에 좋은 방법과 나쁜 방법을 정리해 보았다.
namespace DEL
{
    template <typename T>
    void LoggedDelete(T* t)
    {
        // 로그를 남겨라
        delete t;
    }
}

class Test
{
    //...
private:
    ~Test() {}    // PRIVATE !!!

    friend void DEL::LoggedDelete<>(Test* t);            // 좋다~
    friend void DEL::LoggedDelete<Test>(Test* t);        // 좋다~

    friend void DEL::LoggedDelete(Test* t);              // 이 방법은 사용하지 말도록 하자.
};