본문 바로가기

Library/C/C++

gcc와 Visual C++에서의 템플릿 인자의 가시성 문제

널리 쓰이는 C/C++ 컴파일러 구현에서 가장 중요한 것을 꼽으라면 단연 gcc(g++)와 Visual C++ 컴파일러이다. 이들은 각각 가장 많은 플랫폼과, 가장 널리 쓰이는 크로스 컴파일러 구현이라는 장점을 가지고 있다. 하지만 두 컴파일러 모두 복잡하기 이를데 없는 C++ 표준을 완벽하게 구현하고 있지는 않다. 물론, 이것은 표준이 제대로 안정화되어 있지 않았기 때문이다. 보다 현실적인 문제는, 두 컴파일러가 '소리 없이' 다르게 동작하는 것인데, gcc에서 잘 컴파일되는 코드가 Visual C++에서 제대로 컴파일되지 않거나, 그 반대의 경우가 종종 생긴다. 그 중에서 가장 대표적인 문제를 꼽으라면 템플릿 인자의 가시성 문제를 꼽을 수 있다. 특히, typedef을 사용하여 파생 클래스가 기반 클래스의 정의를 다시 정의했을 때 발생하는 문제이다.

이런 종류의 문제들은 Visual C++ 컴파일러가 C++ 표준이 없던 시절부터 현장에서 쓰이다보니, 궁여지책으로 표준과 상관없이 기능을 구현한 이유가 크다. VC 7.0 이후부터는 최대한 C++ 표준에 맞춰가려는 모습을 보여주지만, 아직도 과거 코드들과의 호환성 때문에 '소리 없는' 오동작을 보여주는 경우가 많다. 다음 코드를 보자.

template< typename Inst >
class Derived : public Base< Inst >
{
public:
    Derived() : Base< int >() {}
    typedef typename Base< Inst >::Inst Inst
};

만약, 이와 같은 코드가 있다면, Derived 클래스에서의 Base< Inst >::Inst에서 Inst 타입은 무엇으로 해석될까? Base< Inst >::Inst일까, 아니면 Dervied의 템플릿 인자인 Inst일까? 상식적으로 생각한다면, 어느 범위에 속하는지 명시적으로 밝혀주었으므로, Base< Inst >::Inst 타입이 되어야 할 것 같다. 그러나, Visual C++에서는, 경우에 따라 Derived의 템플릿 인자인 Inst로 간주된다. 따라서, 만약 Derived의 생성자에서 Base의 생성자를 호출하는 구조라면, 경우에 따라서 두번 초기화가 일어난다는 컴파일 에러를 만날 수도 있다. Visual C++ 2008에서도 이 코드는 제대로 컴파일되지 않는다. 이 문제는 gcc에서는 나타나지 않는데, 사실 템플릿만 제거한 같은 이름의 typedef이 맞는 것인지, 그렇더라도 명시적인 선언이 유효해야 하는지 알 수 없다.

따라서, 이런 컴파일러 사이에서의 잘못된 동작을 최소한 줄이기 위해서는, 가급적 템플릿 인자로 사용된 이름을 다시 사용하지 않는 것이 현명하다. 즉, 위의 코드의 경우, Base 클래스에 Inst으로 typedef된 무엇이 있다면, Derived 클래스에서 템플릿 인자로 이 이름을 사용하거나 typedef에 사용하지 않는 것이 좋다. 다음과 같다.

template< typename Inst >
class Derived : public Base< Inst >
{
public:
    typedef typename Base< Inst >::InstType Derived_Inst; // 가급적 Base::Inst를 다른 이름으로
};


보통, 제대로 코드를 작성했다고 생각했는데도 템플릿 클래스를 사용한 상속 구조에서 C2248이나 CC2437과 같은 Visual C++ 에러를 만난다면 이 문제일 가능성이 높다. C2248 에러는 protected나 private로 선언된 데이터 멤버에 파생 클래스가 접근하려고 했기 때문에 발생하는 에러이며, C2437은 생성자에서 여러 번 기반 클래스의 생성자를 호출했을 경우 발생하는 에러이다. 즉, 이미 생성된 템플릿 클래스의 멤버에 접근하고 있다고 생각하겠지만, 사실 잘못된 템플릿 인자 사용으로 컴파일러가 다른 템플릿 클래스를 생성했으며(이것은 암묵적으로 생성한 것도 아니며, 당연한 컴파일러의 행동이다), 따라서 자신의 멤버에 접근하는 것이 아니라 다른 클래스의 데이터 멤버에 접근하려고 했기 때문에 에러가 발생한 것이다. 이처럼 C2248과 같은 에러는 직접적인 에러지만, 이 문제를 알고 있지 않은 상태에서 C2437과 함께 문제가 발생했다면, 특히 템플릿을 사용했을 때 컴파일 내부 과정을 알 수 없기 때문에 잘못된 부분을 찾아내기 매우 힘들다.