*
MSDN*
Bing 제공
|개발자 센터
MSDN Home   MSDN Home

Pure C++
템플릿과 generics(Generics) 타입의 동작





Stanley B. Lippman (영문)

목차
STL의 기본 요소들
.NET을 위한 STL의 재설계
템플릿은 공유되지 않는 어셈블리 자료형


저는 템플릿과 generic들이 어떻게 동작 혹은 동작하지 않는지를 보여주고, 현재의 Visual C++ 2005에서의 템플릿 구현의 취약점을 말씀드림으로서, 마이크로소프트의 .NET 프레임웍에서의 generics 프로그래밍에 대한 칼럼 연재를 종료하도록 하겠습니다. 저는 STL(Standard Template Library)를 .NET에서도 사용할 수 있도록 하기 위해서 현재 진행중인 작업에 대한 내부적인 논의를 통하여 이 글을 쓰도록 선택 되었습니다. 먼저, 저는 STL의 기본 요소들을 살펴봄으로써 이 글을 읽는 모든 분들이 동일한 위치에서 시작했으면 합니다.


페이지 맨 위로페이지 맨 위로


STL의 기본 요소들 (containers, algorithms, iterators)

STL의 디자인에는 세가지 중요한 요소들이 있습니다: 컨테이너 (container), 알고리즘(algorithms), 반복자(iterator)가 그것들이고, 이 세 가지 요소들을 줄여서 CAI라고 합니다. STL벡터와 리스트 클래스들은 순차적인 컨테이너를 표현합니다. 순차적인 컨테이너는 첫 번째 요소, 두 번째 요소, 그리고 이런 식으로 마지막 요소를 저장하는 방식으로 데이터를 관리 합니다. 예를 들어서, 프로그램에서 어떤 함수의 파라미터 리스트는 흔히 문자열 형식의 인자를 가지고 있는 벡터로 표현됩니다:

vector<string> paramlist;
맵(Map)과 셋(set)클래스는 연관 컨테이너를 나타냅니다. 이러한 연관 컨테이너는 순차 컨테이너에 비해 빠른 검색을 지원합니다. 예를 들어서, 맵에서의 데이터는 키/값의 한 쌍으로 표현될 수 있습니다: 키 값은 검색을 위해서 사용되고, 값은 특정한 데이터를 저장하고, 이렇게 저장된 데이터를 불러오기 위해서 사용됩니다. 이러한 맵을 이용해서, 전화번호부를 표현하기 위해서는, 여러분은 문자열의 값을 가진 키와 숫자 값을 가진 값을 가지는 맵을 선언해야 할 것 입니다. 아래와 선언을 참고해 보시기 바랍니다:
map<string,int> phonedir;

멀티맵은 여러분이 하나의 키 값으로 여러 개의 전화번호를 저장하는 것을 허용해 줍니다. STL은 find, sort, replace, merge 그리고 이러한 컨테이너를 조작하기 위해서 디자인된 몇 개의 알고리즘의 집합을 제공하고 있습니다. 이러한 알고리즘들은 특정 요소의 데이터 형과 그 요소를 저장하고 있는 컨테이너 타입 (예를 들어서, 벡터, 리스트, 아니면 기본으로 제공되는 배열)에 의존하지 않기 때문에 generic 알고리즘이라고 합니다.

이러한 generic 알고리즘은 컨테이너에 간접적으로 동작하기 때문에 컨테이너 독립성을 가지고 있습니다. 그래서 알고리즘에 컨테이너를 직접 전달하기 보다는, 순환의 범위를 알려 주는 반복자(iterator)의 한 쌍(first,last)이 알고리즘 함수 혹은 객체로 전달됩니다. 반복자 한 쌍의 두 번째 요소는 순환을 반복해서 알고리즘이 진행되고 있는 위치가 지정한 범위를 넘어갔을 때를 판별할 수 있는 일종의 보초(sentinel) 혹은 표시자(Marker)의 역할을 해서 알고리즘의 동작을 멈추게 합니다:

sort( paramlist.begin(), paramlist.end() );

위에서 볼 수 있는 코드 중, begin()과 end()는 처음과 마지막 요소 다음에 의 위치를 반환하는 반복자를 반환하는 함수로써, STL 컨테이너의 모든 컨테이너에서 제공됩니다. 예를 들어서, 아래의 선언문들의 순서를 살펴 보시기 바랍니다:

void f()
{
    int ia[4] = {21, 8, 5, 13 };
    vector<int> ivec( ia, ia+4 );  // ivec 벡터를 ia 배열을 이용해 초기화합니다.initialize ivec to ia ...
    list<int>   ilist( ia, ia+4);  // ilist 리스트를 ia 배열로 초기화합니다.initialize ilist to ia ... 
         // ...
}
ia+4가 실제로는 가장 마지막 요소, 13 다음의 주소를 나타내는 것을 주의하시기 바랍니다. (STL을 사용해서 위와 같이 선언하는 것을 처음 해 보시는 분들은 착각으로 인하여 ia+3을 넘겨 줄 수도 있습니다. 하지만, 그렇게 되면 위의 값들 중에서 13은 컨테이너에 포함되지 않게 됩니다.)

반복자는 컨테이너의 요소들을 비교하고, 접근하고, 검색하는데 있어서 동일하고 자료형에 의존하지 않는 방법을 제공해 줍니다. 반복자는 또한 컨테이너의 종류에 상관없이 균일한 포인터 연산 (++, --, *, ==, !=)에 대한 추상화를 제공하는 클래스이기도 합니다:

void f()
{
    // ... same as above ...

    // 각각 똑 같은 generics 알고리즘을 호출합니다...
    sort( ia, ia+4 );                       
    sort( ivec.begin(), ivec.end() );       
    sort( ilist.begin(), ilist.end() );        
}

알고리즘의 sort 함수에 대한 각각의 세 가지 호출은, 피보나치 순열의 네 번째부터 일곱 번째 요소인 5, 8, 13, 21 등의 정렬된 값을 보여 줍니다.

실제로는 각각의 형식적인 제약과 효율을 염두에 두고 생각한다면, 위의 코드는STL에 대한 이상적인 관점만 존재하고 실제로 쓰기에는 무언가 부족함이 있는 코드입니다. 형식적인 제약은 모든 컨테이너들이 모든 STL의 알고리즘을 지원하지는 않습니다. 맵 혹은 셋의 경우는 각각의 요소들의 재배치를 일으켜 컨테이너의 정의를 위반하는 random_shuffle을 지원하지 않고, 스텍의 인덱싱(indexing) 역시 스텍의 구문적 특징을 위반하기 때문에 지원하지 않습니다.

효율 면에서는, generic 알고리즘을 이용하여 리스트 안에 있는 요소들을 정렬 혹은 검색하는 것은 벡터에서 똑 같은 일을 하는 것에 비하여 무거운 연산입니다. 이러한 비효율성 때문에, 리스트 컨테이너는 일반적인 generic 알고리즘 보다 효율적인 자신 만의 클래스 메써드를 제공합니다. 이와 유사하게 특정한 맵 요소를 찾기 위해서 맵에 있는 lookup 메써드를 사용하는 것이 맵의 begin, end 반복자와 키를 넘겨서 find 알고리즘을 이용해 찾는 것 보다 훨씬 빠릅니다.

대부분의 사람들이 모든 컨테이너에 대해서 효율 면에서 동일한 알고리즘을 원하시지만, 실제로는 STL 알고리즘은 리스트 컨테이너나 연관 컨테이너 타입 보다 블락(block) 컨테이너들과 빌트인(built-in) 배열에 좀 더 치우쳐 있습니다. 실제로 제가 벨 연구소에 같이 일을 했던 알렉스 스테파노브(Alex Stepanov: 역자 주-STL을 구현하는데 핵심적인 역할을 했던 개발자입니다.)는 generic 알고리즘을 블락 알고리즘이라고 부르기도 했었습니다.


페이지 맨 위로페이지 맨 위로


.NET을 위한 STL의 재설계

.NET로 STL을 이동하기 위해서 제일 처음 해야 하는 일들은 컨테이너들을 CLR 자료 형으로 다시 구현해야 하는 것 입니다. 여러 가지 이유로, 이 글에서 이유를 충분히 설명할 수는 없지만, 컨테이너를 값 클래스보다 참조 클래스로 만드는 것이 좋습니다. 예를 들어 아래의 코드를 참고해 보시기 바랍니다:

// 여기서는 정의들은 단순화 해서 보여주고 있습니다.
template <class elemType>
ref class vector { ... };

template <class Key, class Value>
ref class map { ... };

네이티브 STL에서는, 컨테이너 모두가 다형성을 지원하지 않습니다. 벡터에 대한 선언은 실제의 벡터 객체를 가져다 줍니다. 아래의 예를 참고해 보시기 바랍니다:

// 기본 STL 벡터
//       ivec.empty() == true 
//       ivec.size() == 0
vector< int > ivec; 

//      ivec2.empty() == false 
//      ivec2.size() == 10
vector< int > ivec2( 10 ); 
그러나, C++/CLI에서는 참조 자료형을 선언 시에, 여러분은 추적할 수 있는 핸들을 정의하는 것과 같습니다-벡터 객체 자체는 매니지드 힙(managed heap)에 머물러 있습니다. 그래서, 이 핸들의 기본값은 nullptr로 설정되어 있습니다. 그림 1을 한 번 눈 여겨 보시기 바랍니다.

또 다른 설계 상의 요구사항은 템플릿을 지원하지 않는 C# 이나 Visual Basic같은 다른 언어들도 이러한 컨테이너를 사용할 수 있어야 한다는 것 입니다. 이러한 요구사항에 대한 가장 단순한 접근 방법은 그림 2에서 보여지는 것과 같이 템플릿 컨테이너가 하나 혹은 그 이상의 시스템 컨테이너 인터페이스를 구현하도록 하고, 이 것을 두 개의 네임스페이스로 나누는 것 입니다.

일반적인 경우, 개발자들은 콜렉션과 generic 인터페이스를 지원해서 현재 콜렉션 인터페이스를 사용하는 클라이언트들이 여러분의 자료형을 이용할 수 있도록 할 것 입니다. 아래에서 두 개의 인터페이스를 지원하기 위해서 어떤 방식으로 선언해야 하는 지가 나와 있습니다:

template <class T>
ref class vector : 
    System::Collections::ICollection,
    System::Collections::Generic::ICollection<T>
{ ... };
시스템 콜렉션 네임스페이스의 컨테이너 인터페이스를 구현하기 위해서는, 여러분은 IEnumerator 과 IEnumerator<T>의 인스턴스를 구현해줘야 합니다:
generic <class T>
ref class vector_enumerator : 
    System::Collections::IEnumerator,
    System::Collections::Generic::IEnumerator<T>
{ ... };

시스템 컨테이너 인터페이스 구현의 나쁜 점은 각각의 요소들에 대한 접근은 가능하지만, STL/CLR 자료형 컨테이너 연산을 할 수 없다는 점 입니다. 그래서, 추가적으로 다른 언어들이 실제의 컨테이너 자료형을 사용할 수 있게 허용해주는 generic 쉐도우(shadow) 컨테이너 자료형이 제공되었습니다. 이렇게 하는 일반적인 방법은 두 가지가 있습니다: Plauger 방법과 Tsao 방법이 그것인데, 이 방법들은 P.J.Plauger과 Anson Tsao의 두 명의 소프트웨어 아키텍트의 이름을 따서 지어졌습니다.

Plauger 방법은 generic 쉐도우 자료형을 제공하는 것으로 요약될 수 있습니다. 즉, 여러분이 쉐도우 generic 클래스를 만들어서 (generic_vector 이라고 불러질 수도 있습니다.), 벡터 템플릿의 복사본을 가지고 있게 하는 것 입니다. 아래에 예제가 있습니다:

generic <typename T>
public ref class vector_generic 
{
     vector<T>^ m_templ;    // 주의...

public:
     vector_generic( vector<T>^ );
};

m_templ의 선언의 의미는 .NET에서 템플릿의 사용에 제한이 있음을 단 적으로 보여주는 것 입니다. 여러분은 generic 자료형에서 인스턴스화(instantiation)를 요구하는 템플릿을 저장할 수 없습니다. 그 이유는 각각 두 개의 파라미터가 있는 자료형(generic 자료형과 템플릿)의 인스턴스화 시점이 각각 틀리기 때문입니다. Generic는 프로그램의 실행 시에 인스턴스화됩니다. 반면에, 템플릿은 컴파일을 할 때 인스턴스화가 됩니다. 그럼으로, 템플릿은 generic 자료형을 가지고 있을 수 있습니다, 이것과 반대로 generic이 템플릿을 가지고 있을 수는 없습니다.

Plauger 방법에 의한 해결책은 템플릿과 generic이 파생된 곳에 공통 generic 인터페이스를 생성하는 것 입니다. 예를 들어서, 그림 3을 참고해 주시기 바랍니다. Tsao 방법에 따른 해결책은 인터페이스가 항상 템플릿 컨테이너(특정 어셈블리에서 인스턴스화 되는)의 인스턴스에 대한 레퍼런스라는 것에 착안해서 나온 방법입니다.

그럼으로, 개발자는 단순히 인터페이스와 템플릿의 구현만 제공하면 됩니다. 아래의 코드에서 보실 수 있는 것처럼, 이 방법을 이용하면 generic 쉐도우 자료형은 제거됩니다.

generic<typename T>
interface class vector_interface : ICollection {...};

template<typename T>
ref class vector : vector_interface, ICollection<T> {...}; 

모든 경우에, 오로지 어셈블리 프로그램 안에서만 있는 사람을 제외한 사람들은 STL/CLR 컨테이너 보다 generic 인스턴스를 통한 방법을 선호합니다. 이 이유는 C++/CLR 하에서의 템플릿이 공개 어셈블리의 멤버가 될 수 없기 때문입니다. 다음 섹션에서 이 것에 대해서 다루기로 하겠습니다.


페이지 맨 위로페이지 맨 위로


템플릿은 공유되지 않는 어셈블리 자료형

.NET이 특정 자료형을 인지하기 위해서는, 두 가지의 요소가 필요합니다: 그 자료형을 표현하는 프로그램 코드가 공통 중간 언어(Common Intermediate Language 이하 CIL)와 그 자료형의 자세한 부분을 묘사하는 메타데이터로 번역되어야 한다는 점 입니다. 유감스럽게도, 템플릿은 .NET으로 둘 중의 어느 부분도 표현될 수 없습니다. 템플릿은, 소멸자(destructor)처럼 .NET에는 존재하지 않습니다.

.NET은 템플릿의 인스턴스만을 알 수 있습니다; 그러나 그 인스턴스가 템플릿의 일종이라는 것은 알 수가 없습니다. 예를 들어서, .NET은 vector<int>와 vector<double>에 대한 메타데이타와 CIL을 볼 수 있습니다. .NET이 볼 수 없는 것은 각각의 인스턴스의 공유된 템플릿 vector을 볼 수 없습니다. 이러한 현상의 한 가지 단점은, 개발자가 템플릿에 대한 리플렉션(Reflection)을 할 수 없습니다. 즉, 개발자가 vector<int> 인스턴스에게 직접적으로 파라미터 리스트와 클래스 정의를 직접적으로 요청할 수 없다는 이야기 입니다.

반면에 generic는 CLR 2.0에서 직접적인 CIL 지원 기능을 가지고 있고, 확장된 리플렉션(reflection)이 generic 리플렉션을 완벽히 지원합니다. 이러한 사실은 여러분이 템플릿을 쓸 것인지 generic 자료형을 사용할 것 인지를 선택할 때, 중요한 고려의 대상이 될 수 있습니다. 두 번째로 고려해야 할 것 은 어셈블리를 간에 공유가 필요한지의 여부입니다. 템플릿은 어셈블리들 간에 공유될 수 없습니다. 그럼으로, 이 사실이 문제가 된다면, 여러분은 템플릿보다 generic을 선택할 수 있습니다. 그렇지 않으면, 제가 STL/CLR 설계에서 이야기했던 크로스 어셈블리 핸드쉐이크(cross-assembly handshake)를 수행하기 위해서 일종의 공통된 인터페이스를 제공해야 합니다.

템플릿인 어셈블리들 간에 인식되지 않는 이유는, .NET이 자료형의 출처를 포함하는 확장된 명기법을 가지고 있기 때문입니다. 즉, .NET 하에서는 자료형은 순수 C++과는 다르게, 공유되는 전역 공간에서 자유롭게 이름을 공유할 수 있는 위치를 가지고 있습니다.

이러한 전역 이름의 오염은 여러 개의 컴포넌트들과 결합되어 때때로 정상적인 응용 프로그램의 동작을 불가능하게 할 수 있습니다. 더 나아가 구분을 해보면: 네임스페이스는 프로그램 수준의 해결책을 제시하고, 자료형의 위치를 추가해 주는 것은 어셈블리 수준의 해결책을 제시해 줍니다.

즉, 전역으로 공유되는 이름들은 어셈블리들이 이름 때문에 발생하는 충돌 없이 합쳐질 수 있기 위해서 각각의 어셈블리 안에 분할되어 있습니다. .NET 에서 자료형은 각각의 위치를 가지고 있습니다. 이것의 의미는 각각의 자료형은 각기 다른 어셈블리 이름으로 표시가 되어 하나의 어셈블리 안에 있는 vector<int>와 두 번째 어셈블리 안에 있는 vector<int>와 동일한 것으로 인식되지 않는다는 것을 의미합니다. generic는 실행 시에 인스턴스화가 CLR에 의해서 제공되기 때문에 이런 문제가 없습니다.

이러한 제약 조건이 있음에도 불구하고, 왜 우리가 템플릿과 STL/CLR 두 개 모드를 제공하는 것을 선택했을 까요? 현업에 있는 C++ 프로그래머들이 현재 존재하는 코드는 물론이고 이 라이브러리에 대한 기술을 계속 쌓아왔기 때문입니다. 마이크로 소프트는 기존에 있는 코드뿐만 아니라 이러한 라이브러리에 대한 전문성에 대한 이동 경로 역시 제공하고 싶었습니다. 만약 이 글을 읽는 여러분이 C++ 프로그래밍에서 지금까지 STL에 많이 의존해 오셨다면, STL/CLR이 없다면 상실감을 느끼셨을 것이 분명합니다. 제가 저번에 들은 바로는 준비가 되면 STL/CLR을 다운로드를 통해서 배포할 계획이라고 합니다. 그러니, 항상 저희에게 귀를 기울여 주시기 바랍니다. 꼭 그렇게 하시리라는 것을 믿어 의심치 않습니다.


페이지 맨 위로페이지 맨 위로



  • 이 문서는 한국 개발자를 위하여 Microsoft MVP 가 번역하여 제공한 문서입니다.
  • Microsoft 는 이 문서를 번역자의 의도대로 제공해 드리며 더 정확한 표현 또는 여러분의 의견을 환영합니다.
  • Microsoft 는 커뮤니티를 위해 번역자의 의도대로 이 문서를 제공합니다.


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

    Stanley B. Lippman는 C++의 창시자인Bjarne Stroustrup과 같이 1984년부터 벨 연구소에서 일을 시작하였습니다. 나중에 디즈니와 드림웍스에서 소프트웨어 테크니컬 디렉터로 일하면서 판타시아 2000의 제작에 관여하기도 하였습니다. JPL의 유능한 컨설턴트로도 일을 했으며, 현재는 마이크로 소프트의 Visual C++ 팀의 아키텍쳐로 근무하고 있습니다.

    페이지 맨 위로페이지 맨 위로QJ: 060414

    Microsoft