Generic 형식 검토

Window Form 2006. 1. 29. 18:41
Pure C++
Generic 형식 검토


템플릿이 CLR(공용 언어 런타임)에서 처리되는 과정에 이상한 일이 발생합니다. 템플릿의 {type} ID가 손실되는 것입니다. 템플릿의 {type} ID가 손실되는 것입니다. 이는 기본 프로그램에서 매크로에 일어나는 일과 유사합니다. C/C++ 컴파일러가 매크로 프로세서 확장을 인식하지 못하는 것처럼 CLR도 템플릿 인스턴스화를 인식하지 못합니다. 두 경우 모두에서 확장은 처리되는 데이터 스트림으로 변환되고 모든 고유한 {type} ID는 사라집니다. 이 부분에서 STL/CLR과 관련하여 디자인상의 차이점이 있는데, 바로 STL(Standard Template Library)이 Visual C++2005용으로 새로 설계된 것입니다. 이는 다음 칼럼에서 다루겠습니다.

반면, generic은 런타임에서 직접 지원됩니다. CIL(Common Intermediate Language) 확장은 generic 형식의 형식 매개 변수, 제약 조건뿐 아니라 generic 형식의 사양도 명시적으로 지원합니다. 또한 시스템 네임스페이스에 있는 기본 클래스 라이브러리의 리플렉션 형식 확장 기능은 generic 형식 및 개체 인스턴스에 대해 완전한 리플렉션 기능을 수행할 수 있도록 합니다. 뿐만 아니라 런타임은 generic 인스턴스의 인스턴스화를 자동으로 선택하여 최적의 방식으로 처리합니다. 이 칼럼에서는 .NET Framework 2.0의 리플렉션 및 generic 리플렉션 지원에 대해 소개하겠습니다.


.NET 형식 정보

대부분의 컴파일러 개발자는 개발 경력이 쌓이다 보면, 보다 효율적인 프로그램 변환 방법을 알아내고 이 효율적인 방법을 지원하기 위한 다양한 인코어(in-core) 프로그램 표현을 작성하며, 마지막으로 완료되었을 때 모든 관련 정보를 폐기하는 데 하루의 상당 시간을 들이고 있다는 사실을 깨닫게 됩니다. 하지만 .NET에서는 이러한 인코어 프로그램 표현이 표준화된 메타데이터 사양 형식으로 유지되며, 런타임(CIL 및 생성된 메타데이터에 대한 완전한 액세스 권한이 있는)에는 물론 "리플렉션"이라는 런타임 검색을 통해 실행 시 사용자도 사용할 수 있습니다.

예를 들어, 다음과 같은 코드를 작성한다고 가정하겠습니다.

enum class test { fail, succeed, untested, rerun };기본 System::Enum 클래스에서는 열거자 이름, 해당 값 등에 액세스할 수 있는 정적 메서드를 제공합니다. 열거자 이름을 해당 정수 값의 오름차순으로 출력하려면 다음과 같이 코드를 작성합니다. for each( String ^s in Enum::GetNames(
status::typeid ))
Console::WriteLine( s );

여기서 작성 방법만 살펴볼 것이 아니라 Enum::GetNames를 호출하면 어떤 일이 발생하는지 생각해 볼 필요가 있습니다. 검색하려는 내용은 enum 상태에 해당하는 열거 이름입니다. 열거 이름을 정수 값만이 담긴 상태 인스턴스와 연결하는 것은 실질적으로 아무 의미가 없습니다. 상태 인스턴스에는 정수 값만 필요하기 때문입니다. 반면, 열거자의 이름과 값은 고정 불변이며 사용자가 정의한 enum의 모든 인스턴스와 관련되어 있습니다. 이는 일종의 메타데이터입니다. 이 메타데이터는 한 번만 저장하면 되며 이 메타데이터가 저장되는 클래스는 System::Type입니다. 즉, System::Type은 enum 상태와 관련이 있는 것입니다.

형식과 연결되어 있는 Type 개체에 여러 가지 방법으로 액세스할 수 있습니다. T::typeid는 지정한 형식 T(이 경우 상태 enum)와 관련된 Type 개체를 반환하는 기본 RTTI(C++ Run-Time Type Information) typeid 연산자의 C++/CLI 확장입니다. 이런 식으로 Type 개체의 런타임 액세스를 통해 메타데이터를 사용하는 것을 앞에서 언급한 대로 리플렉션이라고 합니다.

특정 형식의 개체가 있으면 GetType 인스턴스 메서드를 사용하여 관련 Type을 검색할 수 있습니다. 예를 들어, 다음은 개체의 관련 Type을 검색하고 형식 이름과 정규화된 형식 이름을 모두 표시하는 일반적인 메서드입니다.

void DisplayType( Object^ o )
{
if ( !o ) return;

Type^ t = o->GetType();
Console::WriteLine( "Type is {0} : {1}",
t->Name, t->FullName );
}

그림 1에서는 클래스 generic 형식을 비롯한 여러 형식을 DisplayType 메서드에 전달하는 코드를 보여 줍니다. 이 코드에서는 그림 2와 같은 출력을 생성합니다. 코드 줄을 정리하고 결과에 주석을 달았으며 출력은 최종 제품 릴리스에 있는 것과 똑같지 않을 수 있음을 유의하십시오. Type의 정규화된 이름은 범위 연산자를 C++의 이중 콜론(::)이 아닌 IL(Intermediate Language) 범위 연산자인 점(.)으로 표시합니다. 또한 generic 형식과 관련된 모든 시스템 정보를 확인해야 합니다.

관련 Type 개체를 얻어낼 개체가 없는 경우, 다음과 같이 typeid 연산자를 사용하여 명시적으로 검색할 수 있습니다.

Type^ ts = String::typeid;
Type^ ti = Int32::typeid;물론 관련 네임스페이스에 대한 using 문을 지정하지 않은 경우, 해당 네임스페이스를 명시한 정규화된 형식 이름을 지정해야 합니다. 예를 들면 다음과 같습니다. Type^ tsb = System::Text::StringBuilder::typeid;

실제로 형식이 있는지 알 수 없는 경우에는 명시적인 typeid 연산자를 사용할 수 없습니다. 대신, 형식 이름의 문자열 리터럴을 전달하여 Type 클래스의 정적 GetType 메서드를 사용할 수 있습니다. 이 메서드는 응용 프로그램에서 존재 여부가 확실하지 않은 형식 이름 파일 또는 컬렉션을 읽는 작업 등에 사용할 수 있습니다(그림 3 참조).

리터럴 형식 이름의 형태는 매우 다양합니다. 다음과 같이 비정규화된 이름을 제공하면 형식이 전역 범위에 있는 경우에만 찾을 수 있습니다.

// 비정규화된 이름. System::Math인 경우 찾을 수 없음...
DisplayType( "Math" );(참고: 프로그래머 코멘트는 샘플 프로그램 파일에는 영문으로 제공되며 기사에는 설명을 위해 번역문으로 제공됩니다.)

정규화된 이름을 지정하려면 C++ 범위 연산자 대신 IL 범위 연산자를 사용해야 합니다. 예를 들면 다음과 같습니다.

// 정규화된 이름에 ::을 사용할 수 없습니다.
DisplayType( "System.Math" );

주의할 점은 클래스 generic 또는 클래스 템플릿 형식에는 이런 형태의 검색을 사용할 수 없다는 것입니다. 이는 템플릿 또는 generic 정의가 실제 형식을 나타내는 것이 아니라 실제 형식 인수를 기반으로 인스턴스를 생성하기 위한 패턴에 불과하기 때문입니다.

형식 이름을 검색하기 위한 문자열 리터럴의 세 번째 형식은 이름을 정규화하고 형식이 속해 있다고 판단되는 어셈블리를 지정하는 것입니다. 어셈블리는 다음과 같이 쉼표로 형식과 구분합니다.

// 정규화된 이름 및 어셈블리
DisplayType( "System.Math, mscorlib" );

Assembly 개체가 있는 경우, 다음과 같이 해당 어셈블리 내에 정의된 모든 형식의 Type 배열을 검색하거나 특정 형식을 쿼리할 수 있습니다.

array<Type^>^ types = a1->GetTypes();
Type^ query = a1->GetType( "Query" );

특정 Type이 속해 있는 어셈블리 내에 정의된 나머지 모든 형식을 검색하려면 어떻게 해야 할까요? 또는 특정 개체의 형식 정의가 정의된 어셈블리가 무엇인지 찾아내려면 어떻게 해야 할까요? 또한 형식에 대한 여러 가지 사항을 알고 싶을 때는 어떻게 할까요? 가령, 형식이 배열인지, 클래스인지, generic 형식인지 등을 알려면 어떻게 해야 하는 것일까요? 이러한 정보의 대부분은 Type 개체에서 쿼리하고 검색할 수 있습니다.

Type 클래스는 정말 놀라운 일을 수행합니다. 한마디로 런타임 리플렉션 기능을 여는 열쇠 역할을 한다고 할 수 있습니다. 예를 들어, 그림 4는 형식의 기본 클래스 이름이 있으면 이를 반환하거나 nullptr을 반환하는 간단한 런타임 유틸리티입니다.

Type은 형식이 클래스인지 여부를 판별하는 IsClass와 같은 다양한 쿼리를 지원합니다. 또한 기본 클래스가 있는지, 있을 경우 기본 클래스의 Type은 무엇인지를 반환하는 BaseType 쿼리도 지원하며, 이름(Name,FullName)과 같은 형식의 일반적인 속성을 제공합니다. 필자는 Type을 통해 제공되는 서비스를 일반적인 범주로 분류했는데 이는 다음에 설명하겠습니다.


IsA/HasA 쿼리

IsA/HasA 쿼리를 사용하면 형식이 어떤 것인지 알 수 있습니다. IsA 쿼리의 하나인 IsClass에 대해서는 이미 위에서 소개했습니다. 다른 IsA 속성에는 IsAbstract, IsArray, IsNested, IsPublic, IsSealed, IsSerializable, IsValueType 등이 있습니다. HasA 속성에는 형식에 형식 인수가 있을 때 true를 반환하는 HasGenericArguments, 형식이 다른 형식을 포함하거나 참조할 때 true를 반환(즉, 배열인지, 포인터인지, 참조로 전달되는지 등을 판별함)하는 HasElementType 등이 있습니다. 그림 5에서는 런타임에 generic 형식을 검색하는 방법을 보여 줍니다.

IsGenericTypeDefinition은 정확하게 입력하는 데 어려움이 있기는 하지만 Type이 generic 정의에 해당되는 경우 true를 반환합니다. IsGenericTypeDefinition은 템플릿 형식을 비롯하여 generic 형식이 아닌 형식뿐 아니라 generic 형식의 인스턴스에 대해서도 false를 반환합니다. 예를 들어, 그림 6에서는 세 가지 형식 전달과 해당 출력을 보여 줍니다.

HasGenericArguments는 Type이 generic 정의나 generic 형식 인스턴스를 나타내는 경우 true를 반환합니다. 예를 들어, 다음은 세 가지 같은 형식을 이 HasA 속성에 대해 실행한 결과입니다.

String^은 generic 인스턴스가 아닙니다!
Container`1은 generic 인스턴스입니다!
Container`1은 generic 인스턴스입니다!

HasGenericArguments가 generic 정의 및 generic 인스턴스에 대해 true를 반환하므로 generic 인스턴스만 처리하려면 다음과 같은 일종의 조건문이 필요합니다.

if ( ! t->IsGenericTypeDefinition &&
t->HasGenericArguments )
// generic 인스턴스이면...

GetGenericTypeDefinition은 서비스의 두 번째 범주인 형식의 특수한 측면을 검색하는 부분에 해당됩니다.


검색 내용 가져오기

이는 현재 Type 개체와 관련이 있을 수 있는 특정 형식 또는 형식 컬렉션을 검색합니다. GetGenericTypeDefinition은 그런 메서드 중 하나입니다. generic 형식과 관련이 있는 몇 가지 검색 메서드(빨간색으로 강조 표시)를 예를 들어 설명하기 위해 DisplayGeneric을 자세히 살펴보겠습니다. (그림 7참조)

GetGenericArguments는 각 generic 형식 매개 변수를 나타내는 Type 개체 배열을 반환합니다. 각 매개 변수 요소에는 해당 위치(0부터 시작)가 있으며, 이는 GenericParameterPosition의 반환 값입니다. GetGenericParameterConstraints는 관련 제약 조건 절이 있는 경우, 이를 Type 개체의 배열로 반환합니다. 저는 이 메서드를 사용할 때 실수로 제약 조건에 대해 분명히 하지 않고 개별 매개 변수가 아닌 generic 정의에 적용했습니다. 그 결과 런타임 예외가 발생했습니다.

System.InvalidOperationException: Type.IsGenericParameter가 true인 형식에서 메서드를 호출해야 합니다.

모든 경우에 대해, 이 수정된 메서드에 전달하는 형식을 변경했습니다. 첫 번째 경우, 다음과 같이 클래스 generic에 세 가지 제약 조건을 추가했습니다.

generic <class T>
where T : IComparable<T>,
System::Collections::IEnumerable, ICloneable
ref class Container{};다음과 같이 메서드를 호출하는 경우 Container<String^> ^cs = gcnew Container<String^>;
Type^ gd = cs->GetType()->GetGenericTypeDefinition();
DisplayGeneric( gd );다음과 같은 출력이 생성됩니다. Container`1은 generic 형식 정의입니다!
Container`1의 매개 변수는 1개입니다.

위치 0의 T
T의 제약 조건은 3개입니다: IComparable`1 IEnumerable ICloneable

두 번째 클래스 generic은 System::Collections::Generic 네임스페이스에 있는 SortedDictionary 컨테이너의 인스턴스입니다. 호출 동작은 다음과 같습니다.

SortedDictionary<String^, String^>^ gds =
gcnew SortedDictionary<String^, String^>;
Type^ gdst = gds->GetType()->GetGenericTypeDefinition();
DisplayGeneric( gdst );그러자 다음과 같은 놀라운 결과가 만들어졌습니다(원래는 보다 크고 구체적인 제약 조건 집합을 예상했음). SortedDictionary`2는 generic 형식 정의입니다!
SortedDictionary`2의 매개 변수는 2개입니다.


위치 0의 K
K의 제약 조건은 1개입니다: Object

위치 1의 V
V의 제약 조건은 1개입니다: Object

칼럼을 시작할 때 언급했듯이, Visual C++ 2005의 매개 변수가 있는 형식에 대한 다음(마지막) 칼럼에서는 STL/CLR 라이브러리를 모델로 사용하여 템플릿과 generic의 디자인 상호 운용성에 대해 살펴보겠습니다. 그때까지 여러분의 프로그램이 버그 없이 실행되기를 기원하겠습니다.


Stanley에게 질문이나 의견이 있으면 옆의 메일로 보내시기 바랍니다. purecpp@microsoft.com.

Stanley B. Lippman은 Microsoft Visual C++ 팀의 설계자입니다. Stanley는 1984년, Bell Laboratories에서 C++의 창안자인 Bjarne Stroustrup과 함께 C++ 개발을 시작했습니다. Microsoft에 합류하기 전에는 Disney와 DreamWorks의 장편 애니메이션 작업에 참가했고 JPL의 전문 컨설턴트로 활동했으며, Fantasia 2000의 소프트웨어 기술 책임자를 역임하기도 했습니다.
Posted by 퓨전마법사
,