
Stanley B. Lippman (영문)
목차
매개변수 리스트의 재등장
매개변수 리스트 안에서의 추가적인 템플릿 기능성
형식 매개변수 제약조건
generic 제약조건
최종 정리
Visual Studio® 2005 는 .NET 프레임웍에 형식 매개변수(type parameter) 모델의 generic 프로그래밍을 도입했습니다. C++/CLI는 공통 언어 런타임 (common language runtime: 이하 CLR) generics 와 C++ 템플릿의 두 가지 종류의 형식 매개변수 메커니즘을 지원합니다. 저번 글에서, 저는 두 가지의 메커니즘의 공통적인 특징을 살펴 보았습니다. 이번 호에서는 이 두 개의 메커니즘의 서로 다른 점, 특히 매개변수 리스트와 형식 제약조건 모델의 차이점에 대해서 살펴 보기로 하겠습니다.
매개변수 리스트의 재등장
매개변수 리스트는 함수의 시그니처(signature)로 동작합니다: 이 리스트는 각각의 매개변수 수와 종류를 구별 해 주고, 고유한 식별자로 각각의 매개변수를 연관 지워서, 각 매개변수가 템플릿 정의 안에서 고유하게 참고될 수 있게 합니다.
매개변수는 템플릿의 정의나 generic 형식의 정의 안에서 일종의 저장소 역할을 합니다. 사용자가 매개변수들을 바인드 하기 위해서 실제의 값을 제공하여 객체의 인스턴스를 생성합니다. 매개변수 형식의 인스턴스화는 매크로 확장 메커니즘의 적용과 같은 단순한 문자열의 교환이 아닙니다. 오히려, 실제로 사용자가 제공한 값을 템플릿 혹은 generic 의 정의에서 연관된 공식 매개변수들로 바인딩 하는 것으로 구성되어 있습니다.
generics에서는 각 매개변수들이 Object 나 Object에서 유도된 형식을 나타냅니다. 나중에 보시겠지만, 이것은 여러분이 수행할 수 있는 연산의 종류를 제약하거나 혹은 형식 매개변수들을 통하여 선언된 객체들의 행동을 제한하는 역할을 합니다. 개발자는 자신 만의 명시적인 제약조건을 제공해서 (좀 더 덜 제약적인) 이러한 제약조건을 상쇄시킬 수도 있습니다. 이러한 명시적인 제약조건은 기본 클래스 혹은 실제의 형식 매개변수들이 반드시 유도되어야 하는 인터페이스들의 집합을 참조합니다.
형식 매개변수를 지원하는 것에 덧붙여서, 템플릿은 식(expression)과 템플릿 매개변수를 지원합니다. 덧붙여서, 템플릿은 기본 매개변수 값들을 지원합니다. 이러한 것들은 이름보다는 위치적으로 해석되게 됩니다. 이러한 두 개의 메커니즘 하에서, 형식 매개변수는 class나 typename 같은 키워드가 붙어서 사용되게 됩니다.
매개변수 리스트 안에서 추가적인 템플릿 기능
형식 매개변수에 덧붙여서, 템플릿은 두 가지 다른 매개변수를 허용합니다: 형식이 없는 매개변수와 템플릿 매개변수입니다. 각각에 대해서 간단하게 살펴 보고 가기로 하겠습니다.
형식이 없는 매개변수는 상수 식으로 제한됩니다. 지금 즉시 여러분들의 머리 속에 생각나야 하는 것들은 숫자나 문자열 상수입니다. 예를 들어서, 고정된 사이즈의 스택(stack)을 제공할 것을 결정했을 때, 여러분은 형식이 없는 크기 매개변수뿐만 아니라 기초 형식(element type)를 명시할 것을 결정해서, 기초 형식과 크기로 스택의 인스턴스에 대한 종류를 선택할 수 있습니다. 예를 들어서, 그림 1에서 형식이 없는 매개변수를 가진 고정된 크기의 스택을 한 번 살펴 보시기 바랍니다.
추가로, 템플릿 클래스 디자이너가 하나 혹은 두 개의 매개변수에 대해서 기본 값을 명시해 줄 수 있다면 훨씬 더 편리할 것 입니다. 예를 들어서, 버퍼가 1KB의 사이즈를 가지는 것이 최상이라고 증명된 경우, 템플릿 메커니즘 하에서 아래에서 여러분이 보시는 것과 같이 매개변수에 기본 값이 제공될 수도 있습니다:
// 기본 값을 이용한 템플릿 정의
template <class elemType, int size = 1024>
public ref class FixedSizeStack {};
개발자는 아래와 같이 두 번째 항목에 명시적인 값을 제공해서 기본 크기를 오버라이드 할 수 있습니다:
// 최대 128개의 문자열 인스턴스를 저장하는 스택 FixedSizeState<String^, 128> ^tbs = gcnew FixedSizeStack<String^, 128>;그렇지 않으면, 두 번째 매개변수를 제공하지 않음으로, 아래와 같이 연관된 기본 값을 사용할 수도 있습니다:
// 최대 1024 문자열 인스턴스를 저장하는 스택 FixedSizeStack<String^> ^tbs = gcnew FixedSizeStack<String^>;
이러한 기본 매개변수 값의 사용은 STL의 중요한 설계 개념 중의 하나 입니다. 예를 들어서, 아래의 선언들은 ISO-C++ 표준을 이용해서 선언되었습니다:
// STL에서의 기본 자료 형 매개변수를 사용한 예
{
template <class T, class Container = deque<T> >
class queue;
template <class T, class Allocator = allocator<T> >
class vector;
// ...
}
여러분은 또한 아래와 같이 기본 요소 자료 형을 제공할 수도 있습니다:
// 기본 요소 자료 형을 적용한 템플릿 선언
template <class elemType=String^, int size=1024>
public ref class tStack {};
그러나 위의 선언은 디자인 측면에서는 정당화하기가 좀 더 어려울 것 입니다. 왜냐하면, 일반적으로는 어떤 컨테이너에서 하나의 기본 형식으로 수렴한다고 가정하기 어렵기 때문입니다.포인터는 특정 객체나 함수의 주소가 컴파일 시간에 결정되기 때문에 고정되지 않은 형식 매개변수로 사용될 수 이용될 수 있습니다. 예를 들어, 여러분이 스택 클래스에 어떤 조건이 발생될 때 호출되는 콜백함수를 나타내는 세 번째 매개변수를 제공하기를 원한다면, 적절한 typedef를 이용해서 매우 복잡하게 보이는 정의를 아래와 같이 깔끔하게 보여줄 수 있습니다:
typedef void (*handler)( ... array<Object^>^ );
template <class elemType, int size, handler cback >
public ref class tStack {};
물론, 이 경우 여러분은 현재 존재하는 메써드의 주소를 여러분의 핸들러의 기본 매개변수로 제공해야 합니다. 예를 들어, 여러분이 만든 스택 클래스에 기본 매개변수와 핸들러를 제공한다면 아래와 같이 될 수 있습니다:
void defaultHandler( ... array<Object^>^ ){ ... }
template < class elemType,
int size = 1024,
handler cback = &defaultHandler >
public ref class tStack {};
기본 매개변수들은 위치에 의존하기 때문에, 명시적으로 크기를 제공하지 않고는 콜백 핸들러를 오버라이드할 수 있는 방법은 존재하지 않습니다. 아래에는 여러분의 변경된 스택을 사용할 수 있는 여러 방법이 있습니다:
void demonstration()
{
// 기본 크기와 기본 핸들러
tStack<String^> ^ts1 = nullptr;
// 기본 핸들러
tStack<String^, 128> ^ts2 = gcnew tStack<String^, 128>;
// 세 개의 매개변수를 모두 오버라이드하기
tStack<String^, 512, &yourHandler> ^ts3;
}
두 번째로 템플릿에서 지원되는 추가적인 종류의 매개변수는 템플릿의 템플릿 매개변수입니다. 즉, 템플릿 매개변수 자신이 템플릿을 대신하는 것 입니다. 예를 들어서:
// 템플릿의 템플릿 매개변수
template <template <class T> class arena, class arenaType>
class Editor {
arena<arenaType> m_arena;
// ...
};
에디터 템플릿은 두 개의 템플릿 매개변수를 보여주고 있습니다, arena와 arenaType이 그것 입니다. arenaType은 템플릿 타입의 매개변수고, 여러분은 이 것에 int, String 혹은 다른 사람이 만든 형식, 혹은 여러분이 만든 형식 등을 제공해 줄 수 있습니다. arena는 템플릿의 템플릿 매개변수입니다. 어떤 단일 템플릿 형식 매개변수를 가진 템플릿 클래스도 arena와 결합될 수 있습니다. 그리고 m_arena는 arenaType 템플릿 형식 매개변수와 바인딩되는 템플릿 클래스의 인스턴스입니다. 예를 들어:
// 템플릿 버퍼 클래스
template <class elemType>
public ref class tBuffer {};
void f()
{
Editor<tBuffer,String^> ^textEditor;
Editor<tBuffer,char> ^blitEditor;
// ...
}
형식 매개변수 제약조건
만약 여러분이 여러분의 매개변수화(Parameterized) 된 형식을 제가 지난 달의 스택 클래스에 그랬듯이, 단순히 컨테이너의 저장장소와 그 안의 값들을 다시 얻어오는 용도로만 이용한다면, 그러면 제약조건에 관한 모든 설명들이 무시될 수 있습니다. 이 제약조건은 여러분이 형식 매개변수에서 특정 행동을 호출할 필요가 있을 때(예를 들어, 두 개의 객체를 비교해서 동일한지의 여부를 파악할 필요가 있거나 혹은 여러분이 타입 매개변수를 통하여 메써드 혹은 중첩된 형식의 이름을 호출할 때) 문제가 됩니다. 예를 들어서, 저의 지난 달 글에서 나왔던 typename 선언을 다시 한 번 생각해 보도록 합시다:
template <class T>
ref class Demonstration {
int method() {
typename T::A *aObj;
// ...
}
};
위의 선언이 성공적으로 aObj를 선언함과 동시에, 위의 구문은 또한 여러분의 클래스 템플릿에 결합될 수 있는 형식 매개변수에 대해서 제한을 생성합니다. 예를 들어서, 여러분이 아래와 같은 코드를 작성했을 때, aObj의 선언은 유효하지 않게 되며(특별히 이 경우에 대해서만) 그리고 컴파일러가 에러를 보여주게 됩니다:
int demoMethod()
{
Demonstration<int> ^demi =
gcnew Demonstration<int>( 1024 );
return dm->method();
}
당연하게, 그 제한은 형식 매개변수들이 반드시 A라고 명시적으로 이름이 붙은 중첩된 선언을 가지고 있어야 한다는 점입니다. 만약 typename T::에 B 혹은 C 혹은 Z라고 불리는 선언이 있다고 하더라도 여러분에게는 아무런 이점도 없습니다. 좀 더 일반적인 제약조건은 형식 매개변수들이 반드시 클래스를 대표해야 한다는 점입니다; 그렇지 않으면, T:: 와 같은 스코프 연산자(Scope Operator)의 사용은 허용되지 않습니다. 위에서의 int 형식의 사용은 이러한 제약 조건 두 개를 위반하는 것 입니다. 그래서, Visual C++ 컴파일러는 아래와 같은 에러를 생성할 것 입니다:
error C2825: 'T': must be a class or namespace when followed by '::'
C++ 템플릿에 대한 한 가지 비판은 이러한 형식 제약조건을 설명하기 위한 공식적인 구문이 존재하지 않는다는 점입니다. (Bjarne Stroustrup의 매개변수화된 형식에 대한 원래의 설계 문서에서는, 명시적인 제약조건 구문을 제공하는 것에 대해서 고려한적이 있었으나, 그것에 대해서 불만족스러워 했고, 그 기능을 제공하지 않기로 결정했음을 알 수 있습니다.) 즉, 사용자는 소스 코드 혹은 관련 문서를 읽어 보거나 그렇지 않으면 소스를 컴파일 해서 결과로써 나타나는 에러 메시지를 읽음으로써 암시적인 템플릿의 제약조건을 인지할 수 있습니다.
만약에 템플릿에 맞지 않는 형식을 제공할 필요가 있을 경우, 여러분은 어떻게 하겠습니까? 어떤 면에서는, 여러분이 작성하는 어떤 클래스이든지 특정한 가정이 존재하고, 이러한 가정들은 이 클래스의 사용에 있어서의 제약조건을 대표합니다. 모든 경우에 적절한 클래스를 설계하는 것은 다소 어렵습니다; 모든 경우뿐만 아닌 모든 가능한 형식에 대해서도 적절한 템플릿 클래스를 설계하는 것은 더욱 어렵습니다.
다른 한 편으로는, 사용자에게 약간의 쉴 틈을 주는 몇 개의 템플릿 기능들이 있습니다. 예를 들어서, 클래스 템플릿 멤버 함수는 여러분이 코드에서 그 함수를 사용하기 이전에는 형식 매개변수와 결합되지 않습니다. 그래서, 여러분이 여러분의 형식 매개변수를 무효화하는 메써드를 쓰지 않고 템플릿 클래스를 사용할 수 있다면, 여러분에게 큰 문제가 발생하지는 않을 것 입니다.
그게 가능하지 않다면, 두 번째 대안은 여러분의 형식 매개변수와 특별하게 연관된 특별한 버전의 메써드를 제공하는 것 입니다. 이 경우는, 여러분은 Demonistration<int>::method의 특별화된 인스턴스를 제공하거나 혹은 좀 더 일반적으로는, 여러분은 정수형 매개변수가 제공되었을 때 전체 템플릿 클래스의 특별한 구현을 제공할 수도 있습니다.
일반적으로, 여러분이 매개변수화 된 형식이 아주 많은 형식을 지원할 수 있다고 이야기 할 때, 여러분은 주로 매개변수화(Parameterization)의 수동적인 사용을 이야기 하고 계신 것 입니다. 즉, 적극적으로 그 자료를 조작하는 대신 단순한 저장과 값의 확인이 그것들 입니다.
여러분의 자신의 템플릿을 만든 사람으로써, 자신의 구현이 가지고 있는 형식 매개변수에 대한 암시적인 제약과 이러한 제약들이 너무 많아지지 않도록 하는 것에 대해서 항상 생각하고 있어야 합니다. 예를 들어서, 어떤 형식에 대해서 동등(=)과 좀 더 적은(<) 오퍼레이터의 지원을 요구하는 것은 합리적입니다; 그러나, 좀 더 적거나 같은(<=)과 XOR 비트와이즈 연산자들의 지원을 요구하는 것은 다소 비이성적입니다. (여러분은 인터페이스를 통해서 구현하게 하든지 혹은 함수 혹은 대리자 혹은 함수 객체(functor)를 대표하는 추가적인 매개변수를 요구해서 오퍼레이터의 의존성을 낮출 수 있습니다.) 예를 들어서, 그림 2 는 순수 C++ 프로그래머 기본으로 제공되는 동등(=) 연산자를 이용해서 만들 것 같은 search 메써드의 구현을 보여주고 있습니다.
이 search 함수의 구현에는 잘못된 점이 없습니다. 그러나, 형식 매개변수들이 동등연산자와 많이 결합되어 있기 때문에 다소 템플릿에 덜 친화적입니다. 좀 더 유연한 대안은 사용자에게 비교 연산을 할 수 있도록 객체로 전달하는 것을 허용하는 search 메써드를 제공하는 것 입니다. 여러분은 이것은 함수 멤버 템플릿을 이용해서 할 수 있습니다. 이러한 함수 멤버 템플릿은 추가적인 형식 매개변수를 제공합니다. 그림 3을 한 번 살펴 보시기 바랍니다.
이제 사용자들은 어떻게 컨테이너를 검색할 수 있는 메써드를 선택할 수 있게 되었습니다: 동등 연산자와 완전히 결합되어 좀 더 효율적이나 모든 형식에 대해서는 적절하지 않은 search 함수, 그리고 함수 자체에 전달되어야 하는 비교 형식을 요구하는 멤버 템플릿 search 함수가 바로 그 것 입니다.
어떤 종류의 형식이 이러한 비교 목적에 부합할 까요? 함수 객체는 이러한 목적을 위한 공통적인 C++ 디자인 패턴입니다. 예를 들어서, 여기에 두 개의 문자열 객체가 동일한지를 비교하는 간단하지만 지저분한 함수 객체가 있습니다:
class EqualGuy {
public:
bool operator()( String^ s1, String^ s2 )
{
return s1->CompareTo( s2 ) == 0;
}
};
그림 4의 코드가 여러분이 어떻게 멤버 함수 템플릿 버전의 search와 함께 전통적인 버전의 함수를 호출할 수 있는지를 보여줍니다. 일단 여러분이 템플릿에 대해서 어느 정도 정리가 되셨다면, 여러분은 템플릿을 이용하면 작성하지 못할 것이 거의 없다는 것을 발견하게 될 것 입니다. 아니면, 최소한 기분만이라고 그렇게 느껴지실 것 입니다. Generic 제약조건
템플릿과 다르게, generic 정의에서는 결합되어야 하는 형식 매개변수의 필수 조건을 설명하는 공식적인 구문을 지원합니다. 제가 이 제약조건 기능에 대해서 좀 더 깊숙하게 말씀 드리기 이전에, 왜 템플릿과는 반대로 generic은 제약조건에 대한 구문을 제공하는 기능을 선택했는지에 대한 이유를 간략하게 생각해 보도록 하겠습니다. 제가 생각하는, 주된 이유는, 두 개의 메커니즘 사이의 서로 다른 바인딩 시간인 것 같습니다.
템플릿은 컴파일 시간에 결합이 되어, 그래서 빌드 할 때부터 프로그램이 무효한 형식을 알아차릴 수 있습니다. 사용자는 (컴파일 때 에러가 발생하면) 즉각 문제를 해결하거나 혹은 템플릿이 아닌 전통적인 방법을 사용해서 문제를 해결해야 합니다. 그래서, 프로그램 실행의 전체성에 있어서는 전혀 위험이 되지 않습니다.
이와 반면에, generics는 런타임시에 결합되어, 사용자가 제공한 형식이 유효하지 않다는 것을 발견하게 된다면 시기상으로 너무 늦게 됩니다. 그래서 CLI는 런타임이 유효한 형식만을 바인드 하는 것을 보장하기 위해서 좀 더 정적인-예를 들어서 컴파일 시간 같은-메커니즘이 필요하게 되었습니다. geneic 형식과 연관된 제약조건 리스트는 이러한 컴파일 시간의 필터이며, 혹시 위반되는 형식이 있을 경우는, 프로그램이 빌드 되지 않도록 합니다.
예를 하나 들어보겠습니다. 그림 5 는generic 형식으로 다시 구현된 저의 컨테이너 클래스를 보여줍니다. 이 클래스의 search 메써드는 형식 매개변수들이 IComparable 에서 파생되었다고 가정하기 때문에, 형식 매개변수들은 반드시 CompareTo 메써드의 인스턴스를 구현해야 합니다. (컨테이너의 크기가 생성 시에 사용자에 의해서 제공된다는 사실을 명심하시기 바랍니다. 기억하시겠지만, generics는 형식이 아닌(non-type parameter) 매개변수를 지원하지 않습니다.) 이 generic 클래스 구현은 아래와 같은 치명적인 컴파일 에러를 보여주고 컴파일이 실패합니다:
error C2039: 'CompareTo' : is not a member of 'System::Object'이걸 보시고 여러분이 “그래서요?”라고 혼잣말을 하실지도 모르겠습니다. 아무도 이 매개변수가 System::Object 의 멤버라고 주장하지 않았습니다. 그러나, 이 경우에는, 여러분이 틀렸습니다. 기본으로, generic 형식 매개변수들은 가능한 가장 강력한 제약조건을 만듭니다: 즉, 모든 형식 매개변수들은 Object 형식이 되도록 제약을 합니다. 이 제약조건은 오직 CLI 형식이 generic 형식과 바인드 되는 것이 허용되었고, 그리고 물론, 모든 CLI 형식들이 직접 혹은 간접적으로 Object로부터 파생되었기 때문에 항상 옳다는 것이 보장됩니다. 그래서 기본으로 generic 형식의 생성자로써, 여러분은 매우 안전하지만 제한된 수의 적용이 가능한 연산을 가지게 됩니다.
여러분이 아마도, “유연함이 다 뭐람!, 난 단지 이 코드를 컴파일하고 싶을 뿐이야!”라고 생각하셔서, CompareTo 메써드를 동등연산자로 바꾸어 버릴 수도 있지만, 이러한 일은 이미 심통이 난 컴파일러로 하여금 다시금 아래와 같은 에러 메시지를 보여주게 할 뿐입니다:
error C2676: binary '==' : 'elemType' does not define this operator or a conversion to a type acceptable to the predefined operator다시, 무슨 일이 발생했는가를 설명하자면, 각각의 형식 매개변수들은 자신들의 생명이 비유적으로 Object 객체의 네 개의 공용 메써드(ToString, GetType, GetHashCode, Equals)로 벽처럼 감싸져서 시작됩니다. 효과적으로, 각각의 개별 형식 매개변수들에 대한 제약조건을 열거하는 일은 초기의 강력한 제약조건을 점진적으로 느슨하게 해주는 것을 의미합니다. 다른 방법으로 이야기 하자면, generic 형식을 만드는 사람 입장에서 여러분의 일은 generic 제약조건 목록 규약을 사용해서 형식 매개변수의 허락되는 오퍼레이션을 검증 가능한 방법으로 확장하는 것 입니다. 이러한 일을 어떻게 하는지 한 번 살펴 보도록 하겠습니다.
제약조건의 리스트는 제약조건 조항(constraint clause)라고 불리고, 비 예약어인 “where”를 사용해서 도입되었습니다. 이러한 조항은 매개변수 리스트와 형식 정의 사이에 위치해 있습니다. 실제의 제약조건은 하나 혹은 다수의 인터페이스 형식과 (혹은) 단일 클래스 형식의 이름으로 구성되어 있습니다. 이러한 제약조건은 기본 형식을 대표해서 관련된 형식 매개변수들이 구현되거나 혹은 파생될 것을 기대합니다. 각각의 형식에 대한 공용 오퍼레이션들의 집합은 형식 매개변수에 의해서 수행 가능한 오퍼레이션에 추가됩니다. 그래서, 여러분의 elemType 매개변수가 CompareTo를 호출하도록 허가되기 위해서는, 여러분은 IComparable 인터페이스와 관련된 제약조건 조항을 반드시 아래와 같이 추가해야 합니다:
generic <class elemType>
where elemType : IComparable
public ref class Container
{
// 클래스의 내부는 바뀌지 않았습니다
};
elemType 인스턴스에 의해서 호출이 허용되는 연산을 확장하는, 제약조건 조항은 암시적인 Object 형식의 제약조건과 명시적인 IComparable 제약조건들의 공용(public) 연산의 합집합입니다. 이제 이 generic의 정의는 컴파일되고 실제로 사용할 수 있게 됩니다. 여러분이 실제 형식 매개변수를 구체화할 때, 아래의 코드에서 보는 것과 같이, 실제의 형식 매개변수가 형식 매개변수에 연관된 제약조건과 부합하는지를 검증하는 것은 컴파일러 입니다:
int main()
{
// 문자열과 int는 IComparable를 구현하였습니다. 아래의 선언은 문제 없습니다.
Container<String^> ^sc;
Container<int> ^ic;
// StringBuilder는 IComparable를 구현하지 않았기 때문에 에러가 발생합니다.
Container<StringBuilder^> ^sbc;
}
컴파일러는 sbc의 정의와 같은 어떤 사소한 위반에 대해서도 알려 주지만, 실제의 결합과 generic 형식의 생성은 런타임에 이루어 집니다. generic 형식은 그것의 정의 시점에서(컴파일러가 여러분의 구현을 처리할 때), 그리고 각각의 생성지점에서(컴파일러가 형식 매개변수와 그것에 관련된 제약조건을 확인할 때) 두 부분 모두에서 제약조건을 위반하지 않았음을 검증되게 됩니다. 각각의 지점에서의 실패는 컴파일시의 에러로 귀결됩니다. 제약조건 조항은 각각의 형식 매개변수 마다 각 하나의 엔트리(entry)를 가지게 됩니다. 이러한 엔트리들은 매개변수 리스트와 동일한 순서로 나열 될 것이 요구되지는 않습니다. 매개변수에 대한 다중 제약조건은 콤마에 의해 구별되지만, 하나 이상의 제약조건 목록에서도 콤마를 이용해서 구별할 수 있습니다. 예를 들어:
generic <class T1, class T2, class T3>
where T1 : IComparable, ICloneable, Image
where T2 : IComparable, ICloneable, Image
where T3 : ISerializable, CompositeImage
public ref class Compositor
{
// ...
};
이 경우, 여러분은 인터페이스 형식과 단일 클래스 형식 (각각의 리스트 끝에 나열된)을 구체화하는 세 개의 제약조건을 가지고 있습니다. 이러한 제약조건은 더해지는 성격을 가지고 있어서, 형식 매개변수는 나열된 제약조건의 부분이 아닌 모든 조건을 충족시켜야 합니다. 저의 동료인 “Jon Wray”가 이야기 했듯이, 여러분이 generic 형식의 생성자로써 가능한 연산을 확장해서, 제약조건을 완화하기 위해서는, 차례로 generic 형식의 사용자가 형식 매개변수에 대해서 선택할 때 여러분이 부가하는 제약조건을 증가시켜야 합니다.
T1, T2, T3 조항은 어떤 순서로든지 작성될 수 있습니다. 특정 형식 매개변수에 둘 혹은 그 이상의 조항을 제약조건 조항을 작성하는 것은 허용되지 않습니다. 예를 들어, 아래는 구문 에러로 발생시키게 됩니다:
generic <class T1, class T2, class T3>
// error: 동일한 파라미터에 대한 두 개의 엔트리가 허용되지 않습니다
where T1 : IComparable, ICloneable
where T1 : Image
public ref class Compositor
{
// ...
};
클래스 제약조건 형식은 반드시 sealed 되지 않은 레퍼런스 클래스여야 합니다. (값 클래스(Value Class) 혹은 sealded 클래스는 상속을 허용하지 않기 때문에 허용되지 않습니다.) 이러한 이유로 네 개의 System 네임스페이스 클래스들이 제약조건 조항에 나타나는 것이 금지되어 있습니다: System::Array, System::Delegate, System::Enum, 그리고 System::ValueType. CLI가 오로지 단일 상속 만을 지원하기 때문에, 제약조건 조항은 하나의 클래스 형식의 삽입을 지원합니다. 제약조건 형식은 반드시 generic 형식 혹은 함수 최소한 동일하거나 혹은 그 이상으로 접근할 수 있어야 합니다. 예를 들어서, 여러분은 공용(public) generic 형식에 하나 혹은 그 이상의 제약조건을 internal로 열거할 수 없습니다. 어떤 형식 매개변수들도 제약조건과 결합될 수 있습니다. 여기에 간단한 예제가 있습니다:
Any of the type parameters can be bound to a constraint type. Here is a simple example:
generic <class T1, class T2>
where T1 : IComparable<T1>
where T2 : IComparable<T2>
public ref class Compositor
{
// ...
};
또한 제약조건은 상속되지 않습니다. 예를 들어서, 제가 Compositor라는 클래스로부터 상속을 받는다면, Compositor의 T1, T2의 IComparable 제약조건들은 BlackWhite_Compositor 클래스의 동일한 이름의 매개변수들에 적용되지 않는다는 말입니다:
generic <class T1, class T2>
public ref class BlackWhite_Compositor : Compositor
{
// ...
};
매개변수들이 기본 클래스와의 결합에 사용될 때 이러한 제약조건이 상속되지 않는 점이 설계 상 불편한 점 중의 하나 입니다. Compositor의 전체성을 보장하기 위해서는, BlackWhite_Compositor는 반드시 Compositor의 하부객체에 전달되는 모든 매개변수들에 대한 Compositor 제약조건들을 전달해야 합니다. 예를 들어서, 정확한 사용은 아래와 같이 됩니다:
generic <class T1, class T2>
where T1 : IComparable<T1>
where T2 : IComparable<T2>
public ref class BlackWhite_Compositor : Compositor
{
// ...
};
최종 정리
여러분이 위에서 보셨듯이, C++/CLI하에서는, 여러분은 CLR generic 인지 혹은 C++ 템플릿인지를 선택할 수 있습니다. 여러분은 여러분 자신의 특별한 필요에 바탕을 둔 선택을 할 수 있는 정보를 가지고 있어야 합니다. 이러한 두 개의 메커니즘에서는, 매개변수화 된(parameterized) 단순한 저장과 각 요소의 조회를 넘어서 각각의 형식 매개변수가 반드시 지원해야 하는 오퍼레이션에 대한 가정을 가지고 있습니다.
템플릿을 이용해서는, 이러한 가정들이 암시적입니다. 이것은 템플릿을 만드는 사람으로 하여금 구현을 할 때 매우 큰 자유도를 가져다 주는 혜택을 줍니다. 그러나, 이 점은 허용되는 형식 매개변수들의 문서화되지 않은 제약조건들을 자주 만나야 한다는 점에는 템플릿의 실제 사용자들에게 큰 혜택을 주지는 않습니다. 이러한 제약조건의 위반이 컴파일 시에 에러로 나타나고 그래서 실제 실행 시의 프로그램 실행의 전체성에 위협이 되지는 않지만, 템플릿 클래스의 사용은 아주 힘든 일이 될 수도 있습니다. 이 메커니즘에 대한 설계 성향은 구현자(implementor)에 초점을 두어 그 쪽으로 기울여져 있습니다.
generics를 이용하면, 이러한 가정들은 명시적으로 그리고 제약조건 조항 안에서 기본 형식의 집합이 나열되어 연관되어야 합니다. 이렇게 하는 것은 런타임 엔진에게 형식 생성을 위해서 전달되는 어떤 generic도 정확하다는 것을 검증할 수 있다는 면에서 generic의 사용자에게 이점을 줍니다. 이것은 generic 을 만드는 사람에게 설계 자유도에 있어서 어떤 제약조건들에 있어서는 지원하기 어려운 템플릿 디자인 구문을 만듭니다. 이러한 정의 시에 혹은 사용자 구체화한 형식 매개변수들의 공식적인 제약조건의 위반은 컴파일 시간의 에러를 발생시킵니다. 이 메커니즘에 대한 설계 성향은 자료구조를 이용하는 소비자(Consumer)에 초점을 두어 그 쪽으로 기울여져 있습니다.
스탠리에게 질문과 하실 말씀은 purecpp@microsoft.com.로 보내 주시기 바랍니다.
