Silverlight를 설치하려면 여기를 클릭합니다.*
Korea 대한민국변경|Microsoft 전체 사이트
MSDN
|개발자 센터
MSDN 홈 > MSDN 칼럼 > Deep C# > 부분적인 템플릿 구현

부분적인 템플릿 구현

Bobby Schmidt
Microsoft Corporation

이 칼럼에서는 Microsoft Visual C++ .NET에는 아직 없는 C++ 표준을 따르기 위해 필요한 템플릿 기능에 대한 여러 가지 사항에 대해 설명합니다.

Visual C++에 없는 일치 기능의 일부 목록을 보려면 설명서  를 참조하십시오. 이 목록은 "Visual C++ 구현이 C++ 표준과 일치하지 않는 것으로 알려진 위치 중 일부"를 보여 줍니다. 특별히 눈에 띄는 것은 "일부"라는 단어입니다. 따라서 여기서는 게시된 목록에 없는 몇 가지 일치하지 않는 문제점을 다룹니다.

클래스 템플릿의 부분 특수화

클래스 템플릿에는 정확히 하나의 기본 선언이 있습니다.

template<typename T>
class X
    {
    };

x<int>    x1;
x<char>   x2;
x<char *> x3;

이 기본 템플릿은 잠재적으로 무한한 템플릿 인수의 집합에 대해 일반적입니다. 위의 기본 템플릿은 개념적으로 다음과 같이 작성하는 것과 같습니다.

template<typename T>
class X<T> // 개념상으로만 가능합니다. C++ 문법에서는 허용되지 않습니다.
    {
    };

템플릿에 여러 개의 명시적 특수화도 포함할 수 있습니다. 각 특수화에서는 모든 템플릿 매개 변수를 템플릿 인수의 고유한 치환 값으로 명시적으로 고정시킵니다.

template<typename T>
class X//<T>
    {
    };

template</*typename T*/>
class X<char>
    {
    };

x<int>    x1;
x<char>   x2; // 명시적 특수화 X<char>를 사용합니다. 
x<char *> x3;

일반과 특수의 양극단 사이에 C++ 표준이 부분 특수화를 호출하는 중간 방법이 있습니다. 이름에서 알 수 있는 바와 같이, 부분 특수화는 템플릿 매개 변수를 부분적으로 고정합니다.

template<typename T>
class X
    {
    };

template<typename T>
class X<T *>
    {
    };

template<>
class X<char>
    {
    };

X<int>    x1;
X<char>   x2;
X<char *> x3; // 부분적 특수화 X<T *>를 사용합니다.

Visual C++ .NET은 위 예제에 대해 자멸합니다.

'T' : 선언되지 않은 식별자
구문 오류 : '>'
구문 오류 : '{' 앞에 ';'이 없습니다. 
구문 오류 : '}' 앞에 ';'이 없습니다.
템플릿 클래스의 중첩 UDT를 줄을 넘어서 정의할 수 없습니다.
이전 오류를 복구할 수 없습니다. 컴파일이 중지됩니다. 

명시적 특수화와 마찬가지로 부분 특수화도 제한된 집합의 인수와 일치합니다. 이런 의미에서 명시적 특수화는 가장 제한된 부분 특수화입니다. 그리고 기본 선언처럼 잠재적으로 무한 집합의 인수와 일치합니다. 때론 이런 무한 집합이 겹치므로 같은 템플릿 인수가 여러 부분 특수화와 일치할 수 있습니다.

template<typename T>
class X<T *>
    {
    };

template<typename T>
class X<T const *>
    {
    };

X<char const *> x4; // 일치하는 것은 무엇입니까?

이런 모호성을 해결하기 위해 컴파일러는 C++ 표준에 정의된 일치 및 순위 기준을 사용하여 후보 부분 특수화를 이른바 "부분 순서"로 순위를 매깁니다. 이 순위에 따라 특수화 X<T const *>는 성공합니다.

C++ 표준 라이브러리를 비롯한 현대의 많은 C++ 라이브러리에서는 템플릿 부분 특수화가 필요합니다. 우리가 판매하는 컴파일러는 이 점이 부족하므로 이 라이브러리는 Visual C++에서 전혀 작동하지 않거나 손상된 구문에서 작동합니다. 예상하는 바와 같이 이 문제는 다음 버전의 Visual C++에서 우선적으로 수정할 것입니다.

함수 템플릿의 부분 순위

표준에서는 오버로드된 함수 템플릿 간의 부분 순위도 정의합니다.

template<typename T>
void f(T)
    {
    }

template<typename T>
void f(T *)
    {
    }

template<>
void f(char)
    {
    }

int main()
    {
    f(6);   // f(T) 사용
    f("6"); // f(T *) 사용
    f('6'); // f(char) 사용
    }
참고   f(T *) 선언은 오버로드되며 부분 특수화가 아닙니다. 반대되는 지식에 관계없이 "함수 템플릿의 부분 특수화와 같은 것은 없습니다." 예, 함수 템플릿의 "명시적" 특수화는 없습니다(예: f(char)). 아니오, 클래스 템플릿과 함수 템플릿 간에 용어가 일치하지 않는 이유를 알 수 없습니다.

Visual C++에서는 다음과 같이 함수 템플릿의 부분 순위를 잘못 처리합니다.

int main()
    {
    f(6);   // 좋습니다. f<T>를 제대로 사용합니다.
    f("6"); // 오류입니다. 사용할 f를 결정할 수 없습니다.
    f('6'); // 좋습니다. f<char>을 제대로 사용합니다.
    }

표준에서 동일한 함수 템플릿 순위에 따라 클래스 템플릿 순위를 정의한다는 점을 고려하면 이런 결함은 예상할 수 있습니다. Visual C++에서 후자를 지원하면 결과적으로 전자는 제외됩니다.

이것은 연습용으로 남겨두겠습니다.

template<typename T>
void f(T const *)
    {
    }

template<>
void f(char *)
    {
    }

int main()
    {
    f("어떤 오버로드와 일치합니까?");
    }

클래스 템플릿 매개 변수 이름

클래스 템플릿의 기본 정의에서는 일반적으로 템플릿 매개 변수를 이름으로 표시합니다.

template<typename T> // T는 템플릿 매개 변수입니다.
class X
    {
    void f(T); // T를 여기에 기술합니다.
    };

그러나 명시적 특수화는 다음과 같습니다.

template<> // 템플릿 매개 변수가 없습니다.
class X<int>
    {
    void f(T); // 아직 작동합니까?
    };

이 특수화에서 실제적으로 이름을 선언하지 않더라도 매개 변수 이름 T가 여전히 범위 안에 있고 이름 확인을 위해 사용할 수 있습니까? C++ 표준에 따르면 대답은 "아니오"입니다. Visual C++ .NET의 경우 대답은 "예"입니다. 위의 코드는 특수화가 선언된 것처럼 컴파일되고 실행됩니다.

template<>
class X<int>
    {
    typedef int T;
    void f(T);
    };

멤버-템플릿 정의

일부 클래스와 클래스 템플릿은 자체도 템플릿인 멤버를 포함할 수 있습니다. 다른 유형의 멤버와 마찬가지로 C++ 표준에서는 다음과 같이 멤버-템플릿 정의가 선언으로 표시되거나,

class X
    {
    template<typename T>
    T f(T)
        {
        return T();
        }
    };

다음과 같이 선언과 별도로 표시될 수 있습니다.

class X
    {
    template<typename T>
    T f(T);
    };

template<typename T>
T X::f(T)
    {
    return T();
    }

Visual C++ .NET 설명서  에 따르면 둘째 예제는 컴파일될 수 없습니다. 그러나 이 설명서는 잘못되었습니다. 이 예제는 컴파일됩니다. 그러나 X 자체가 보통 클래스가 아닌 템플릿인 경우 다음과 같습니다.

template<typename T2>
class X
    {
    template<typename T>
    T f(T);
    };

template<typename T2>
template<typename T>
T X<T2>::f(T)
    {
    return T();
    }

멤버가 연산자인 경우는 다음과 같습니다.

class X
    {
    template<typename T>
    operator T();
    };

template<typename T>
X::operator T()
    {
    return T();
    }

그리고 예상한 대로 Visual C++ .NET에서는 오류가 발생합니다.

해결 방법: C# 또는 Java에서 멤버에 대해 해야 하는 것처럼 멤버 선언 시 공격적인 멤버를 정의합니다.

class X
    {
    template<typename T>
    operator T()
        {
        return T();
        }
    };

멤버 템플릿의 명시적 특수화

다른 템플릿과 마찬가지로 멤버 템플릿은 명시적 특수화를 포함할 수 있습니다. 위에 있는 최근 예제에 다음을 추가합니다.

class X
    {
    template<typename T>
    T f(T)
        {
        return T();
        }
    };

template<>
int X::f(int)
    {
    return int();
    }

Visual C++에서는 이 코드에 대해 다음 오류를 생성합니다.

'int X::f(int)' : 'X'에서 오버로드된 멤버 함수를 찾을 수 없습니다.

오류를 제거하기 위해 다음과 같이 X에 특수화를 선언할 수 있습니다.

class X
    {
    template<typename T>
    T f(T)
        {
        return T();
        }
    template<>
    int f(int);
    };

이 코드는 여전히 중단됩니다. 그 이유는 표준에 따르면 선언은 X를 포함하는 네임스페이스 범위(이 경우 전역 범위)에 표시되어야 하기 때문입니다.

이 문제에 대한 해결 방법은 없습니다. 현재로서는 템플릿 멤버를 특수화할 수 없다는 것을 알 수 있습니다.

클래스 템플릿 내에 중첩된 클래스

표준에 따르면 클래스 템플릿은 중첩 클래스 선언을 포함할 수 있습니다.

template<typename T>
class X
    {
    class Y;
    };

Visual C++에서는 이와 더불어 중첩 정의도 허용합니다.

template<typename T>
class X
    {
    class Y
        {
        }
    };

Y 정의를 X 외부로 이동시킬 경우,

template<typename T>
class X
    {
    class Y;
    };

template<typename T>
class X<T>::Y // 여기서 오류가 발생했지만 괜찮습니다.
    {
    };

컴파일러는 다음과 같이 잘못 반응합니다.

'T' : 선언되지 않은 식별자
템플릿 클래스의 중첩 UDT를 줄을 넘어서 정의할 수 없습니다
이전 오류를 복구할 수 없습니다. 컴파일이 중단됩니다. 

이 메시지는 부분 특수화 예제에서 생성된 메시지와 같습니다.

이 문제를 이전 항목의 특수화 문제와 연결하면 딜레마에 빠지게 됩니다. 템플릿과 관련된 일치 문제를 해결하기 위해선 선언에서 멤버를 정의해야 할 수도 있고 선언과 별도로 멤버를 정의해야 할 수도 있습니다. 이런 제한을 모두 우회하기 위해 적용할 수 있는 어느 경우에나 맞는 한 가지 규칙은 없습니다.

이런 경우에 Visual C++ .NET이 매우 적당하지만, 같은 스타일의 규칙을 어디에나 적용할 필요는 없습니다. 제가 C++을 처음으로 배울 적에는 거의 항상 다음과 같이 멤버 정의를 선언과 분리시켰습니다.

class X
    {
public:
    X(int = 10, char = '\0');
    X &operator=(X const &);
    long const *f() const;
    // ...
    };

X::X(int i, char c)
    {
    // ...
    }

X &X::operator=(X const &that)
    {
    // ...
    }

long const *X::f() const
    {
    // ...
    }

C 프로그래머인 제게는 위 코드가 더 자연스런 C답게 보입니다. 클래스 정의는 압축되어 있어 빨리 참조할 수 있는 반면 함수 정의는 펼쳐져 있어 읽기가 더 쉽습니다. 또한 C 코드를 이런 스타일의 C++ 코드로 이식하는 것도 훨씬 빨라졌습니다. 별도의 빈 줄과 반복되는 함수 선언으로 코드를 채우면 적절한 비용으로 명확성과 이식이라는 이점을 얻을 수 있는 것으로 보였습니다.

템플릿의 결과에 따라 내 스타일에 대해 다시 생각해 보니 같은 결과를 얻는 데 필요없는 높은 비용이 드는 것으로 나타났습니다.

template<class T>
class X
    {
public:
    X(int = 10, char = '\0');
    X &operator=(X const &);
    long const *f() const;
    // ...
    };

template<class T>
X<T>::X(int i, char c)
    {
    // ...
    }

template<class T>
X<T> &X<T>::operator=(X const &that)
    {
    // ...
    }

template<class T>
long const *X<T>::f() const
    {
    // ...
    }

STL 및 STLesque 템플릿에 직면했을 때 결국 다음과 같이 작성하기로 결정했습니다.

template<typename T = int, typename P = T *>
class X
    {
public:
    template <unsigned N = 10>
    class X2
        {
    public:
        template<typename T2>
        static P f(T2 const &)
            {
            return new T[x][N];
            }
        };
    };

골절을 악화시키진 않았지만 꽤 아팠습니다.

template<typename T = int, typename P = T *>
class X
    {
public:
    template <unsigned N = 10>
    class X2;
    };

template<typename T, typename P>
template<unsigned N>
class X<T, P>::X2
    {
public:
    template<typename T2>
    static P f(T2 const &);
    };

template<typename T, typename P>
template<unsigned N>
template<typename T2>
P X<T, P>::X2<N>::f(T2 const &x)
    {
    return new T[x][N];
    }

종속 이름 확인

템플릿 내에서 일부 형식과 식은 템플릿 매개 변수에 따라 의미가 달라집니다.

template<typename T, unsigned N>
void f()
    {
    T a[N];
    sizeof a;
    }

int main()
    {
    f<int, 7>();
    }

f의 본문에서 형식 T와 상수 식 N은 호응하는 템플릿 매개 변수에 따라 사소하게 나마 변경됩니다. 또한,

  • a의 형식은 "N T의 배열"이므로 TN에 종속됩니다.
  • 상수 식 sizeof aa의 형식에 직접적으로 종속되므로 TN에 간접적으로 종속됩니다.

컴파일러는 f 정의의 컨텍스트만으로는 TN의 의미를 계산할 수 없으므로 대신 f를 인스턴스화할 때 TN을 고정시켜야 합니다. 따라서 a의 형식과 크기도 인스턴스화할 때까지는 알 수 없습니다. 내 예제에서 호출

f<int, 7>();

template<>
void f<int, 7>()
    {
    int a[7];
    sizeof(a);
    }

를 인스턴스화하고 TN을 고정시키므로 a의 형식과 크기를 고정시킵니다.

이와 같은 두 단계의 메커니즘은 두 갈래의 오류 메시지를 만들 수 있습니다. 호출을

f<int, 0>();

으로 변경하면 컴파일러는 템플릿 인수 0을 템플릿 매개 변수 unsigned N으로 변경합니다. 그러나 f<int, 0> 정의에서는 배열이 크기 0으로 정의될 수 없으므로 컴파일러는

int a[0];

을 형성할 수 없습니다. Visual C++ .NET에서는 이 호출을 제대로 컴파일하지 못하고 정의 부분과 인스턴스화 부분 모두에서 오류 메시지를 생성합니다.

좀 더 복잡한 예제

다음 코드에서도 Visual C++ .NET은 오류를 생성합니다.

template<typename T>
struct X
    {
    X(T *p = 0)
        {
        f(p);
        }
    };

class Z;

X<Z> x;

f는 무엇을 의미할까요? 한 눈에 fX 정의에 표시된 항목을 나타내며, 따라서 종속되지 않는 이름이라는 것을 알 수 있습니다. 이러한 "어떤 항목"을 선언한다면 다음과 같을 것입니다.

void f(void *);

template<typename T>
class X
// ...

x 정의는 다음을 효과적으로 인스턴스화합니다.

template<>
struct X<Z>
    {
    X(Z *p = 0)
        {
        ::f(p);
        }
    };

다음은 Z를 네임스페이스로 이동시킵니다.

// ...

namespace N
    {
    class Z;
    }

X<N::Z> x;

x는 여전히 다음으로 인스턴스화됩니다.

template<>
struct X<N::Z>
    {
    X(N::Z *t = 0)
        {
        ::f(t);
        }
    };

마지막으로 하나 이상의 멤버를 네임스페이스에 추가합니다.

// ...

namespace N
    {
    class Z;
    void f(Z *);
    }

X<N::Z> x;

Visual C++ .NET은 여전히 f::f로 잘못 확인합니다. fN::f로 확인해야 하므로 인스턴스화는 다음과 같이 됩니다.

template<>
struct X<N::Z>
    {
    X(N::Z *t = 0)
        {
        N::f(t);
        }
    };

적절한 컴파일러는 다음과 같은 경우에도 비정규화된 fN::f로 마법과도 같이 해석합니다.

  • 이름 N이 템플릿 매개 변수에 직접 바인딩되지 않는 경우(네임스페이스를 템플릿 인수로 사용할 수 없습니다.)
  • 이름 NX 정의의 컨텍스트에 표시되지 않는 경우
  • 비정규화된 이름 fX가 인스턴스화 시점에 표시되지 않는 경우

이 마법은 표준에서 "인수 종속 이름 조회"를 호출하는 것이나 나머지에서 "Koenig 조회"를 호출하는 것을 포함합니다. 이런 조회를 통해 함수의 인수에 종속된 범위에서 함수 이름을 찾을 수 있습니다. Koenig 조회를 사용하면 이름 확인이 훨씬 복잡해집니다. 동시에

cout << x

와 같이 다른 경우에는 임의의 x에 대해 불가능한 식을 사용할 수 있습니다.

Visual C++ .NET에는 Koenig 조회와 관련된 문제가 있지만 템플릿의 범위를 벗어나므로 나중에 다른 칼럼에서 다루겠습니다.

export 및 내보낸 템플릿

끔찍하군요. 정말 끔찍합니다.

export에서는 지금까지 알아 본 "포함 모델" 템플릿과는 다른 "분리 모델" 템플릿을 사용할 수 있습니다.

  • 포함 모델인 경우 템플릿은 인스턴스화 시점 전에 인스턴스화하는 번역 단위에서 정의되거나 포함되어야 합니다.
  • 분리 모델인 경우 템플릿은 하나의 번역 단위에서 정의되고 별도의 다른 번역 단위에서 인스턴스화될 수 있습니다.

다음은 비정상적으로 작동하는 필수적인 예제입니다.

export template<typename T>
class X;

Visual C++ .NET은 이것을 처리할 수 없습니다. 실제로 이 글을 쓴 시점(지난 7월)에 판매 및 배포된 상용 컴파일러 중에 이것을 처리할 수 있는 프로그램은 없었습니다. C++ 표준에서 요구하는 모든 언어 기능 중에 export와 내보낸 템플릿은 구현되지 않는 가장 최신의 기능입니다.

이 미개척지는 진출하기 시작하고 있습니다. EDG(Edison Design Group)의 마법사는 C++ front end  의 버전 3.0을 출시했는데 이 번역기는 내 예제를 컴파일할 수 있습니다. 이미 설명한 바와 같이 EDG는 완전한 컴파일러를 구현하지는 않지만 이 기술을 포함시키려는 다른 사용자에게 자사의 프런트 엔드에 대한 사용권을 제공합니다. EDG 번역기를 사용하는 업체 중에 Greg Comeau가 상용 export 인식 컴파일러를 개발하는 데 가장 근접  해 있습니다.

export는 5년 동안 표준에 포함되어 있었지만 이제서야 구현되는 상태에 있으며, 기능의 복잡성, 비용 및 상대적인 장점 면에서 약점이 드러나고 있습니다.

export는 입증되고, 널리 알려지고, 인정된 방법의 코드화가 아닌 C++ 위원회의 이론적 창안으로 탄생했습니다. 위원회에서 창안한 다른 모든 중요한 언어 기능(예: 예외, 네임스페이스, RTTI, 템플릿)은 언어 사용자와 언어 구현자가 모두 예측하지도 의도하지도 못한 결과를 초래했습니다. export가 이와 같은 불행한 유산을 따를 것이라고 힘 주어 주장할 수 있는 증거는 많습니다. 해결되어야 할 문제는 해결되지 않고, 실제로는 이 문제를 더 악화시키며, 번역 단위의 구문과 이름 확인을 손상시키고, 미래의 언어 혁신을 어렵게 하고, 완전히 지정하기가 불가능해 보이며, 실제로는 구현하기에 비용이 많이 듭니다. 정말 끔찍한 일입니다.

Visual C++ .NET의 비일치 문제 중에 export가 없는 문제는 염려할 필요가 없습니다. 컴파일러에서 export를 지원한다 해도 사용하고 싶지 않을 것입니다. 확신이 서지 않거나 많은 사람들이 export를 실패한 실험으로 생각하는 이유를 알고 싶으면 C/C++ Users Journal  에 오는 9월과 11월에 게제할 Herb Sutter의 칼럼을 읽어 보십시오.

결론

이 칼럼에서는 템플릿과 관련된 중요한 문제인 C++ 표준 14절에 포함된 문제를 설명합니다. 다음 칼럼이나 그 다음 칼럼에서는 특별히 템플릿과 관련되지 않은 일치 문제를 자세히 다루겠습니다.

Erratica

Visual C++ .NET에 새로 사용되는 일치 기능을 논의한 이전 칼럼  에는 다음과 같은 비형식 템플릿 매개 변수의 예제가 있습니다.

template<int N>
class X;

X<10> x;

그리고 이 예제가 Visual C++ 6.0에서 작동한다고 했는데 실제로는 작동하지 않습니다.

이런,

제가 원래 그 칼럼을 쓸 때 이 예제는 비일치였습니다. x 선언의 시점에 형식 X<int>(이)가 완료되지 않습니다. Visual C++ 6.0에서 실패하는 것은 괜찮습니다. 내가 작성하려고 한 예제는 다음과 같습니다.

template<int N>
class X
    {
    };

X<10> x;

이와 같이 수정된 예제는 실제로 Visual C++ 6.0에서 작동하며 이점도 좋습니다. 컴파일러에는 좋지만 예제로 사용하기에 적절하지 않으므로 칼럼 내용으로는 좋지 않습니다. 따라서 해당 칼럼에서 예제를 삭제하겠습니다.

 


Deep C++ .NET

Bobby Schmidt는 MSDN에 매우 강렬한 기술적인 산문을 기고하고 있습니다. 편집자와 칼럼리스트로 활동하고 있는 "C/C++ Users Journal"에서 그의 매력적인 다른 칼럼을 볼 수 있습니다. 과거에는 라디오 DJ, 야생 동물 보호관, 천문학자, 수영장 관리인, 사립 탐정, 신문 배달부, 대학 강사 등의 경력을 갖고 있습니다.



최종 수정일: 2002년 09월 03일
Top of Page Top of Page


Microsoft