2016년 3월 19일 토요일

[c++기초] 7. 템플릿

31-1.함수 템플릿

31-1-가.타입만 다른 함수들

C++은 여러 가지 개발 방법을 지원하는 멀티 패러다임 언어라고 하는데 적어도 다음 세 가지 방법으로 개발을 할 수 있다.

① 구조적 프로그래밍 : C언어에서와 마찬가지로 함수 위주로 프로그램을 작성할 수 있다. C++이 C언어의 계승자이므로 C언어의 개발 방법을 지원하는 것은 당연하다.
② 객체 지향 프로그래밍 : 캡슐화, 추상화를 통해 현실 세계의 사물을 모델링할 수 있으며 상속과 다형성을 지원하기 위한 여러 가지 언어적 장치를 제공한다.
③ 일반화 프로그래밍 : 임의 타입에 대해 동작하는 함수나 클래스를 작성할 수 있다. 객체 지향보다 재사용성과 편의성이 더 우수하다.
일반화 프로그래밍은 주로 C++ 템플릿에 의해 지원되며 C++ 표준 라이브러리가 일반화의 좋은 예이다. 템플릿은 C++이 일반화를 위해 제공하는 가장 기본적인 문법이므로 템플릿에 대한 이해는 C++ 표준 라이브러리인 STL을 이해하기 위한 문법적 토대가 된다. 개념은 간단하지만 실제 적용될 때는 굉장히 복잡한 형태를 띄기 때문에 원리를 이해하는 것이 중요하다.
템플릿(Template)이란 무엇인가를 만들기 위한 형틀이라는 뜻이다. 플라스틱 모형을 만들기 위한 금형이라든가 주물을 만들기 위한 모래틀이 형틀의 예이며 좀 더 이해하기 쉬운 예를 들자면 붕어빵을 만드는 빵틀을 들 수 있다. 템플릿은 모양에 대한 본을 떠 놓은 것이며 한 번만 잘 만들어 놓으면 이후부터 재료만 집어 넣어서 똑같은 모양을 손쉽게 여러 번 찍어 낼 수 있다. 길거리의 붕어빵 장사들을 보면 빵틀에 밀가루와 팥만 집어 넣어서 똑같이 생긴 붕어빵을 얼마든지 찍어 내고 있지 않은가?
템플릿의 또 다른 특징은 집어 넣는 재료에 따라 결과물들이 조금씩 달라진다는 것이다. 금형에 플라스틱을 집어 넣으면 플라스틱 제품이 나오고 고무를 집어 넣으면 고무로 된 제품을 만들 수 있다. 방틀에도 밀가루를 넣으면 붕어빵이 나오지만 찹쌀 가루를 넣으면 잉어빵이라는 좀 더 부가가치가 높은 상품이 만들어진다. 제품의 모양만 같을 뿐이지 내용물은 조금씩 달라지는 것이다.
함수 템플릿은 함수를 만들기 위한 형틀이라고 생각하면 된다. 비슷한 모양의 함수들을 여러 개 만들어야 한다면 각 함수들을 매번 직접 정의할 필요없이 함수 템플릿을 한 번만 만들어 놓고 이 템플릿으로부터 일련의 함수들을 찍어낼 수 있다. 다음 예제는 일정한 타입의 변수 두 개의 값을 교환하는 Swap 함수를 만든다.

  : SwapFunc
#include <Turboc.h>

void Swap(int &a, int &b)
{
     int t;
     t=a;a=b;b=t;
}

void Swap(double &a, double &b)
{
     double t;
     t=a;a=b;b=t;
}

void main()
{
     int a=3,b=4;
     double c=1.2,d=3.4;
     Swap(a,b);
     Swap(c,d);
     printf("a=%d,b=%d\n",a,b);
     printf("c=%f,d=%f\n",c,d);
}

main에서 변수 여러 개를 선언한 후 Swap 함수로 값을 교환하고 확인을 위해 출력했다. 정수형, 실수형 변수들이 애초에 선언된 값과 반대로 바뀌어 있음을 확인할 수 있다.

a=4,b=3
c=3.400000,d=1.200000

두 값을 교환하는 알고리즘은 무척 간단해서 두 변수의 값을 서로 대입하기만 하면 된다. 단, 먼저 대입받는 변수의 값을 잠시 저장해 놓기 위한 임시 변수 하나가 필요하며 실인수의 값을 바꿔야 하므로 포인터나 레퍼런스를 이용한 참조 호출을 해야 한다. 음료수잔의 콜라, 사이다를 교환하고 싶다면 빈 컵 하나가 반드시 필요하며 교환 대상에 따라 빈 컵의 모양과 크기도 달라야 한다. 음료수를 교환하기 위한 빈컵으로 소주잔은 적당하지 못하다. 예제에는 정수에 대한 Swap, 실수에 대한 Swap 함수가 작성되어 있는데 교환 대상의 타입이 달라지더라도 알고리즘은 동일하며 본체 내용 중 달라지는 부분은 인수와 임수 변수의 타입 뿐이다.
int와 double외에 char, long, 사용자 정의 구조체 등의 변수들도 교환해야 한다면 각 타입에 대해서도 Swap 함수를 일일이 만들어야 할 것이다. 알고리즘은 같지만 인수와 임시 변수의 타입이 다르므로 한 함수로 임의 타입의 변수를 교환할 수는 없다. 그나마 C++은 오버로딩을 지원하므로 함수의 이름이라도 똑같이 작성할 수 있지만 C에서는 함수의 이름마저도 SwapInt, SwapDouble 등으로 달라야 한다. 이런 비슷한 함수들을 일일이 만들어야 한다는 것은 무척 짜증나는 일이며 만든 후에 수정하기도 번거롭다. 그래서 이 함수들을 통합할 수 있는 여러 가지 방법들을 생각해 볼 수 있다.

 우선 인수의 타입을 #define이나 typedef로 정의한 후 본체에서는 이 매크로를 참조하는 방법을 생각할 수 있다. 교환 대상에 대한 중간 타입을 정의하고 함수에서는 중간 타입을 사용하는 것이다. 필요할 때마다 매크로의 타입 정의를 바꾸면 임의의 타입에 대해 교환하는 함수를 만들 수 있다. 다음이 그 예이다.

#define SWAPTYPE int
void Swap(SWAPTYPE &a, SWAPTYPE &b)
{
     SWAPTYPE t;
     t=a;a=b;b=t;
}

SWAPTYPE이 int로 정의되어 있으므로 현재 Swap 함수는 int형 변수값을 교환하지만 SWAPTYPE을 double로 바꾸면 실수를 교환하는 함수로 탈바꿈할 것이다. 그러나 이 방법은 컴파일할 때마다 필요한 타입으로 바꿔야 한다는 점이 불편하다. 쉽게 말해서 자동이 아니라 수동이다. 또한 이 방법은 하나의 매크로가 두 개의 값을 가질 수 없으므로 각 타입을 교환하는 함수가 동시에 두 개 이상 존재할 수 없다는 점이 문제다.
 두 번째로 다음과 같은 매크로 함수를 쓰는 방법도 가능하다. 중간 타입을 쓰는 것이 아니라 아예 함수 자체를 매크로로 만들어서 필요할 때마다 전개하는 방식이다.

#define SWAP(T,a,b) { T t;t=a;a=b;b=t; }

이 매크로 함수는 잘 동작하기는 하지만 매크로내에서 임시 블록 변수 t를 선언해서 사용하므로 교환 대상의 타입을 일일이 가르쳐 줘야 한다. 그래야 임시 변수 t의 타입을 결정할 수 있다. 정수값 a와 b를 바꾸려면 SWAP(int, a, b)로 호출해야 하는데 첫 번째 인수로 전달되는 int라는 타입이 왠지 불편해 보이고 최소 의사 표시 원칙에도 맞지 않다. 군더더기없이 교환하고자 하는 대상만 지정할 수 있어야 한다.
또한 매크로 함수는 치환될 때마다 코드가 반복되므로 프로그램이 커지는 고질적인 문제가 있다. 그래서 복잡한 동작을 하는 함수에는 부적합하며 값을 교환하는 SWAP 정도의 초간단 함수에만 적용할 수 있다. 게다가 매크로 함수는 여러 가지 부작용도 많아 일반적인 용도로 쓰기에는 한계가 있다.
 이외에 void *라는 일반적인 포인터 타입을 쓰는 방법도 있다. void *는 임의의 타입을 가리킬 수 있으므로 교환 대상 변수의 번지를 전달하여 메모리 복사하는 방식으로 두 값을 교환할 수 있다. 실제로 이런 방식이 가능한지 예제를 만들어 보자.

  : SwapVoid
#include <Turboc.h>

void Swap(void *a,void *b,size_t len)
{
     void *t;
     t=malloc(len);
     memcpy(t,a,len);
     memcpy(a,b,len);
     memcpy(b,t,len);
     free(t);
}

void main()
{
     int a=3,b=4;
     double c=1.2,d=3.4;
     Swap(&a,&b,sizeof(int));
     Swap(&c,&d,sizeof(double));
     printf("a=%d,b=%d\n",a,b);
     printf("c=%f,d=%f\n",c,d);
}

실행 결과는 앞의 예제와 완전히 동일한데 두 개의 함수를 만들지 않아도 한 함수로 정수형과 실수형을 모두 교환할 수 있다. 함수가 포인터를 요구하므로 호출측에서는 교환대상에 일일이 &를 붙여 번지를 넘기고 또한 길이도 같이 전달해야 한다. void &라는 것은 없으므로 임의 타입을 전달할 때는 레퍼런스를 쓸 수 없고 포인터만 가능하다. void *는 임의의 변수가 있는 번지를 가리킬 수 있어 타입에 대한 정보는 불필요하지만 대신 길이에 대한 정보가 없으므로 길이도 같이 전달하는 수밖에 없다.
Swap 함수 내부도 다소 복잡한데 임의의 타입을 교환해야 하므로 단순한 대입으로는 값을 교환할 수 없으며 변수가 차지하고 있는 영역끼리 메모리 복사를 통해 교환해야 한다. 이때 교환을 위한 임시 변수도 반드시 동적으로 할당해야 하는 부담이 있는데 교환 대상의 길이를 전혀 예측할 수 없으므로 충분한 길이의 임시 버퍼로는 안전하지 않다. 16바이트 정도면 왠만한 기본 타입은 다 교환할 수 있겠지만 1000바이트짜리 구조체가 전달될지도 모르기 때문에 동적으로 할당해야 한다. 이 방법대로라면 아주 큰 배열까지도 교환할 수 있다.
void *를 이용한 교환 함수는 나름대로 실용성도 있고 그야말로 임의의 타입을 다룰 수 있다는 점에서 훌륭하다. 실제로 이런 함수는 종종 사용되며 템플릿보다 더 우월한 면도 있다. 하지만 일일이 &를 붙여 번지를 전달해야 하고 길이까지 가르쳐 주어야 한다는 점에서 불편하기는 마찬가지이다.

여러 가지 대안들이 있지만 신통하게 마음에 드는 방법은 딱히 없다. 지금까지 이 문제에 대한 전통적인 해결방법은 복사한 후 원하는 부분을 수정하는 이른바 몸으로 떼우기 작전밖에 없었다. 약간의 수고만 감수하면 Swap(int, int)를 복사한 후 Swap(double, double)이나 Swap(unsigned, unsigned)를 얼마든지 만들 수 있다. 복사된 수만큼 함수가 늘어나기는 하지만 적어도 호출할 때마다 함수의 본체가 반복되지는 않으며 완전한 함수이므로 지역변수를 자유롭게 쓸 수 있고 복잡한 동작도 얼마든지 가능하다. & 연산자가 없어도 되며 길이 정보도 전달할 필요가 없다.
그러나 이런 전통적인 방법은 필요한 타입이 늘어날 때마다 사람의 작업을 필요로 하므로 생산성이 떨어지며 또한 일부를 수정하지 않는 실수의 가능성이 있어 위험하기도 하다. 이런 복사 후 수정 작업을 컴파일러가 대신 하는 문법적 장치가 바로 함수 템플릿이다. 원하는 함수의 모양을 템플릿으로 등록해 두면 함수를 만드는 나머지 작업은 컴파일러가 알아서 한다. 다음 예제는 Swap 함수를 템플릿으로 정의한 것이다.

  : SwapTemp
#include <Turboc.h>

template <typename T>
void Swap(T &a, T &b)
{
     T t;
     t=a;a=b;b=t;
}

struct tag_st {int i; double d; };
void main()
{
     int a=3,b=4;
     double c=1.2,d=3.4;
     char e='e',f='f';
     tag_st g={1,2.3},h={4,5.6};

     printf("before a=%d, b=%d\n",a,b);
     Swap(a,b);
     printf("after a=%d, b=%d\n",a,b);
     Swap(c,d);
     Swap(e,f);
     Swap(g,h);
}

Swap 함수 템플릿을 정의한 후 정수, 실수, 문자열, 구조체 등에 대해 Swap 함수를 호출해 보았다. 임의의 타입에 대해 Swap 함수를 사용할 수 있되 단 함수 내에서 지역적으로 선언된 타입은 사용할 수 없다. 지역 타입은 함수 내부에서만 쓰는 것이므로 함수간의 통신에는 사용할 수 없기 때문이다. 그래서 tag_st 구조체를 전역으로 선언했는데 이 구조체 선언문이 main 함수 안에 포함되면 에러로 처리된다. 모든 타입에 대해서 제대로 동작하는데 정수형의 a, b에 대해서만 결과를 확인해 보았다. 실행 결과는 다음과 같다. 나머지 타입들도 출력해 보면 잘 교환될 것이다.

before a=3, b=4
after a=4, b=3

함수 템플릿을 정의할 때는 키워드 template 다음에 <> 괄호를 쓰고 괄호안에 템플릿으로 전달될 인수 목록을 나열한다. 템플릿 인수 목록에는 키워드 typename 다음에 함수의 본체에서 사용할 타입의 이름이 오는데 함수의 형식 인수와 비슷한 기능을 한다고 생각하면 된다. 이 이름은 명칭 규칙에만 맞으면 마음대로 작성할 수 있으나 일반적으로 T나 Type이라는 짧은 이름을 많이 사용한다. 이어지는 함수의 본체에서 템플릿 인수를 참조하여 구체적인 코드를 작성한다.
함수 호출부에서 int 타입을 사용했으면 T는 int가 되며 함수 본체에서 참조하는 T는 모두 int가 될 것이다. 마찬가지로 double이 전달되면 T는 double이 되고 char가 전달되면 T는 char가 된다. 호출부에서 전달되는 실제 타입을 템플릿 정의에서 표기하기 위한 임시적인 이름이 바로 typename T인 것이다. 템플릿이 빵틀이라면 T는 빵틀에 집어넣는 재료에 비유될 수 있다.
템플릿 인수 목록에는 키워드 typename 대신 class를 쓸 수도 있으며 구형 컴파일러들은 이 자리에 class를 사용했었다. template <typename T>와 template <class T>는 같은 표현이다. 어차피 클래스도 타입이고 int, double 등도 일종의 클래스이므로 의미상 틀리지는 않지만 이렇게 되면 반드시 클래스 타입만 가능한 것처럼 보여 오해의 소지가 있다. 그래서 새로 개정된 표준에는 좀 더 일반적인 의미를 가지는 typename이라는 키워드가 새로 도입되었으며 가급적이면 class 대신 typename을 사용하는 것이 좋다. 현재 class라는 키워드는 클래스를 정의할 때만 쓰도록 권장된다.
템플릿이란 컴파일러가 미리 등록된 함수의 형틀을 기억해 두었다가 함수가 호출될 때 실제 함수를 만드는 장치이다. 그렇다면 다음과 같이 함수를 만드는 매크로 함수를 정의하는 것과는 어떤 점이 다를까?

#define MakeSwap(T) \
void Swap(T &a, T &b)\
{\
     T t;\
     t=a;a=b;b=t;\
}

struct tag_st {int i; double d; };
MakeSwap(int)
MakeSwap(double)
MakeSwap(char)
MakeSwap(tag_st)

MakeSwap 매크로 함수로 타입 T를 전달하면 T 값 두 개를 교환하는 함수 Swap이 만들어진다. 문법적으로는 분명히 가능한 방법이며 템플릿과 개념상 비슷하지만 두 방법은 지원 주체의 레벨이 다르다. 매크로 함수는 전처리기가 처리하지만 템플릿은 컴파일러가 직접 처리한다. 전처리기는 지시대로 소스를 재구성할 뿐이므로 개발자가 필요한 타입에 대해 일일이 매크로를 전개해야 하므로 수동이지만 템플릿은 호출만 하면 컴파일러가 알아서 함수를 만드는 자동식이므로 매크로 함수보다는 역시 한수 위이다.
그래도 MakeSwap 매크로 함수는 그럴 듯해 보이기는 하는데 함수는 궁한대로 이 방법을 쓸 수도 있다. 그러나 같은 방법으로 클래스를 정의하는 매크로 함수는 만들 수 없다. 왜냐하면 함수는 이름이 같아도 타입이 다르면 오버로딩할 수 있지만 클래스는 오버로딩이 안되기 때문이다. ## 연산자를 쓰면 가능은 하겠지만 타입에 따라 클래스의 이름이 매번 달라지므로 쓰기에 불편하다.
흔하지는 않지만 템플릿 인수 목록에서 두 개 이상의 타입을 전달받을 수도 있다. 함수의 형식 인수 개수에 제한이 없듯이 함수 본체에서 변화가 생길만한 타입이 둘 이상이라면 함수 템플릿도 여러 개의 인수를 가질 수 있다. 이때는 원하는만큼 typename을 반복하되 각 타입의 이름은 구분할 수 있도록 다르게 작성해야 한다.

template <typename T1, typename T2>

당연한 얘기가 되겠지만 함수 템플릿 정의는 함수 호출부보다 먼저 와야 한다. 함수 템플릿 정의문에 의해 컴파일러는 임의의 타입 T의 값을 교환하는 함수의 모양을 Swap이라는 이름으로 기억할 것이다. 만약 main을 더 앞쪽에 두고 싶다면 순서를 바꿀 수 있되 템플릿에 대한 원형을 호출부의 앞쪽에 미리 선언해야 한다. 템플릿 함수의 원형은 template 키워드부터 시작해서 템플릿 함수의 선두를 그대로 가져간 후 세미콜론만 붙이면 만들 수 있다.

template <typename T>
void Swap(T &a, T &b);

일반 함수와 원형을 만드는 방법은 동일한데 원형 선언이 두 줄에 걸친다는 점에서 다소 어색해 보이기는 한다. 물론 한 줄에 붙여써도 별 이상은 없다. 이 함수 템플릿으로부터 실제 함수가 어떻게 만들어지는지는 다음 항에서 연구해 보자.


31-1-나.구체화

함수 템플릿은 어디까지나 함수를 만들기 위한 형틀에 지나지 않으며 그 자체가 함수인 것은 아니다. 컴파일러는 함수 템플릿 정의문으로부터 앞으로 만들어질 함수의 모양만 기억하며 실제 함수가 호출될 때 타입에 맞는 함수를 작성한다. 함수 템플릿으로부터 함수를 만드는 과정을 구체화 또는 인스턴스화(Instantiation)라고 하는데 호출에 의해 구체화되어야만 실제 함수가 만들어진다. 존재하는 모든 타입에 대해 함수를 미리 만들어 놓는 것이 아니다.
이때 함수 템플릿으로부터 만들어지는 함수를 템플릿 함수라고 한다. 용어가 비슷해서 다소 헷갈리는데 둘 다 뒤쪽에 강세를 두고 읽으면 실체 파악이 쉽다. 함수 템플릿은 함수를 만들기 위한 템플릿이고 템플릿 함수는 템플릿으로부터 만들어지는 함수이다. 배열 포인터, 포인터 배열 등의 용어도 마찬가지인데 한국말은 대체로 뒤쪽 단어에 진짜 뜻이 있으며 끝까지 들어 봐야 무슨 말인지 알 수 있다. 용어 중간에 서술어를 넣어서 이해하면 잘 외워진다.
컴파일러가 템플릿으로부터 함수를 구체화를 하는 방법은 사람의 몸으로 떼우기 작전과 사실상 동일하다. 소스를 분석하는 중간 단계에서 템플릿의 정의를 잘 기억해 두었다가 호출되는 함수들을 템플릿으로부터 일일이 생성해 낸다. 호출부의 인수를 보고 주인님이 뭘 원하는지 알아내며 템플릿에 이 재료를 집어 넣어 함수를 찍어내는 것이다. 사람이 해야 할 잡다한 작업을 컴파일러가 대신하는 것 뿐이다.
만약 템플릿만 정의하고 함수를 호출하지 않으면 아무런 일도 일어나지 않으며 템플릿 자체는 메모리를 소모하지 않는다. 마치 붕어빵틀이 붕어빵이 아니어서 먹을 수도 없고 재료를 소모하지 않는 것과 마찬가지이다. 호출에 의해 템플릿이 구체화되어 실제 함수가 될 때만 프로그램의 크기가 늘어난다. 호출되지도 않는 함수를 만들 필요는 전혀 없는 것이다. 템플릿만 선언해 놓고 비주얼 C++로 맵 파일을 만들어서 확인해 보면 과연 그렇다는 것을 확인할 수 있다.
SwapTemp 예제를 통해 템플릿 함수가 구체화되는 것을 확인해 보자. main에서 정수, 실수, 문자, 구조체 등 각각의 타입으로 Swap 함수를 호출하는데 이때마다 컴파일러는 Swap 함수 템플릿을 참조하여 실인수의 타입에 맞는 실제 Swap 함수를 구체화한다. 이 예제의 경우 네가지 버전의 함수가 구체화될 것이다.
맵 파일을 만들어 확인해 보면 과연 4개의 Swap 함수가 작성되어 있음을 눈으로 직접 확인할 수 있다. 맵 파일(Map File)은 함수나 변수가 어느 주소에 배치되었는지에 대한 일종의 컴파일 결과 보고서인데 프로젝트 설정 페이지의 링크 탭에서 옵션을 선택하면 Debug 디렉토리에 *.map 파일로 생성된다. 물론 long, short, char *, float 등등 다양한 타입에 대해 Swap 함수를 호출하면 더 많은 Swap 함수들이 생성될 것이다.

0001:000001b0       ?Swap@@YAXAAH0@Z           004011b0 f i SwapTemp.obj
0001:00000200       ?Swap@@YAXAAN0@Z           00401200 f i SwapTemp.obj
0001:00000260       ?Swap@@YAXAAD0@Z           00401260 f i SwapTemp.obj
0001:000002b0       ?Swap@@YAXAAUtag_st@@0@Z   004012b0 f i SwapTemp.obj

컴파일러에 의해 구체화된 함수는 실행 파일에 실제로 존재하며 컴파일 단계에서 미리 만들어지므로 실행시의 부담은 전혀 없다. 함수가 호출될 때 만들어지는 것이 아니다. 대신 매 타입마다 함수들이 새로 만들어지므로 구체화되는 수만큼 실행 파일의 용량이 늘어난다. 템플릿은 크기를 포기하는 대신 속도를 얻는 방식인데 크기와 속도는 항상 반비례 관계에 있다.
타입만 다른 함수들을 직접 복사해서 정의하는 방법과 함수 템플릿을 정의한 후 컴파일러가 구체화하도록 하는 것과는 어떤 차이점이 있을까? 일단은 반복되는 부분이 통합되므로 소스 길이가 짧아지고 수정할 필요가 있을 때 템플릿만 수정하면 된다. 따라서 관리하기 편리해진다는 이점이 있으며 이후 임의의 타입에 대한 함수를 새로 구체화하는 것도 컴파일러가 알아서 하므로 확장성도 훨씬 더 좋다.
더 이상 사용하지 않는 함수를 삭제하는 것도 컴파일러의 몫이다. Swap(c, d) 호출문을 삭제하고 재컴파일해 보면 Swap(double, double) 함수는 다시 생성되지 않음을 확인할 수 있다. 그러나 함수 템플릿을 쓴다고 해서 실행 파일의 크기가 작아지는 것은 아님을 유의하자. 구체화되는 함수들은 각자가 메모리를 따로 차지하므로 실행 파일의 크기면에서는 별다른 이점이 없다. 복사해서 수정하는 방법과 똑같이 메모리를 차지한다. 이런 면에서 볼 때 템플릿보다는 오히려 SwapVoid 예제의 방식이 메모리 절약면에서는 유리한데 SwapVoid는 어떤 타입에 대해서건 함수는 딱 하나밖에 생성되지 않는다.

명시적 인수 지정

컴파일러는 호출부의 실인수 타입을 판별하여 필요한 함수를 구체화하는데 예를 들어 Swap(a, b)는 a, b가 정수이므로 Swap(int, int) 함수를 구체화할 것이고 Swap(c, d)는 c, d가 실수형이므로 Swap(double, double) 함수를 구체화할 것이다. 템플릿 타입 정의에 의해 두 인수의 타입은 같아야 하므로 SwapTemp 예제에서 Swap(a, c)는 두 인수의 타입이 int, double로 달라 에러로 처리된다. Swap(a, c) 호출에 대해 Swap(double, double) 함수를 구체화하고 a를 double로 암시적 변환해서 호출할 수도 있을 것 같지만 템플릿은 타입이 정확해야 하므로 암시적 변환까지는 고려하지 않는다.
상수는 변수와 달리 그 형태만으로 타입을 정확하게 판단하기 힘든 경우가 있다. 그래서 템플릿 함수를 호출할 때 실인수와는 다른 타입을 강제로 지정할 수 있는데 이때는 함수명 다음의 < > 괄호안에 원하는 타입을 밝힌다. 다음 템플릿 함수는 큰 값을 조사한다.

template <typename T>
T Max(T a, T b)
{
     return (a > b) ? a:b;
}

Max(3, 4)는 두 인수가 정수형이므로 Max(int, int) 함수를 구체화하여 호출할 것이다. 그러나 Max<double>(3, 4)로 호출하면 실인수 3, 4가 정수형 상수지만 산술 변환되어 Max(double, double) 함수가 호출된다. 물론 Max(3.0, 4.0)이라고 호출해도 마찬가지이다. 정수형 변수 둘에 대해 실수형 Max를 호출하고 싶으면 Max((double)a, (double)b)로 캐스트 연산자를 사용할 수도 있다.
리턴 타입이나 인수로 직접 사용되지 않는 타입을 가지는 함수를 호출하기 위해서는 명시적으로 템플릿의 인수 타입을 지정해야 한다. 리턴 타입은 호출할 함수를 결정할 때는 사용되지 않으며 또한 인수로 전달되지 않고 함수 내부에서만 사용하는 타입도 함수 호출문에는 나타나지 않는다. 이럴 때는 컴파일러가 함수 호출문만으로 구체화할 함수를 결정할 수 없으므로 어떤 타입의 템플릿 함수를 원하는지를 분명히 지정해야 한다.

  : TempReturn
#include <Turboc.h>
#include <iostream>
using namespace std;

template <typename T>
T cast(int s)
{
     return (T)s;
}

template <typename T>
void func(void)
{
     T v;

     cin >> v;
     cout << v;
}

void main()
{
     unsigned i=cast<unsigned>(1234);
     double d=cast<double>(5678);

     printf("i=%d, d=%f\n",i,d);
     func<int>();
}

cast는 인수로 전달된 s를 템플릿 인수가 지정하는 타입으로 캐스팅하는 함수이다. cast(1234) 호출문만으로는 어떤 버전의 함수를 만들지 결정할 수 없으므로 명시적으로 인수를 밝혀서 호출해야 한다. 1234라는 상수는 cast의 정수형 인수로 고정되어 있을 뿐이지 T를 결정하는데는 사용할 수 없다. 이 예제의 경우 unsigned cast(int), double cast(int) 두 버전의 함수가 구체화되는데 이 두 함수는 이름이 동일하고 인수 목록까지 같으므로 오버로딩 조건을 만족하지 못한다. 이처럼 리턴 타입만 다른 경우라도 템플릿에 의해 각각 따로 구체화될 수는 있지만 호출할 때 어떤 함수를 호출하는지를 반드시 밝혀야 한다.
그냥 cast(1) 이라고 호출해 버리면 어떤 함수를 원하는지 결정할 수 없어 모호하므로 에러로 처리된다. 1234라는 정수 상수를 unsigned형으로 캐스트하고 싶다면 cast<unsigned>(1234)로 호출하고 5678이라는 정수 상수를 double 타입으로 캐스트하려면 cast<double>(5678)로 호출한다. cast라는 템플릿 이름만으로는 정보가 부족하다.
func 함수는 내부적인 처리를 위해 T형의 지역변수 v를 선언하여 사용한다. 물론 T가 가변적인 타입이므로 본체는 전달된 모든 타입에 대해 가능한 코드만 사용해야 한다. func는 인수도 리턴값도 없으므로 호출부만 봐서는 도대체 어떤 함수를 구체화할 지 전혀 결정할 수 없다. 따라서 func()라고 호출하면 컴파일러는 뭘 원하는지 어리둥절해 할 것이다. 이때도 func<int>() 처럼 지역변수 v의 타입을 명시적으로 전달해야 한다.
리턴 타입만 다른 템플릿이나 알지도 못하는 타입의 지역변수를 선언하는 함수는 그다지 실용성이 없어 보이고 저런 걸 어디다 쓸까 싶지만 호환되는 여러 가지 타입의 객체 중 원하는 것을 선택해서 대신 생성해 주는 래퍼 함수를 만들고 싶을 때 이런 기법이 가끔 사용되기도 한다. STL을 연구하다 보면 이런 함수를 실제로 볼 수 있는데 그때를 위해 이런 문법도 있다는 것은 기억해 두도록 하자.
명시적 인수 지정 기법은 비슷 비슷한 함수를 여러 벌 만들지 않고 특정한 한 타입에 대해서만 함수를 구체화하고 싶을 때도 아주 실용적이다. 다음 예제를 보자.

  : ExplicitPara
#include <Turboc.h>

template <typename T>
void LongFunc(T a)
{
     // 아주 긴 함수의 본체
}

void main()
{
     int i=1;
     unsigned u=2;
     long l=3;

     LongFunc(i);
     LongFunc(u);
     LongFunc(l);
}

LongFunc은 본체가 굉장히 큰 함수이고 길이가 길다고 할 때 int, unsigned, long 각각에 대해 이 함수를 일일이 구체화하면 실행 파일의 용량이 무시못할 정도로 커질 것이다. 이 외에도 int와 호환되는 타입은 char, short, 열거형 등 아주 많은 타입이 있는데 사실 이 타입들은 int와 거의 똑같은 방법으로 처리할 수 있으므로 굳이 본체를 따로 만들 필요까지는 없을 것이다. 이럴 때는 호출문을 다음과 같이 작성하여 구체화되는 함수의 수를 줄일 수 있다.

LongFunc<int>(i);
LongFunc<int>(u);
LongFunc<int>(l);

실인수의 타입에 상관없이 LongFunc(int) 함수만 구체화되며 실인수가 정수로 산술 변환되어 전달된다. 물론 이 경우 정수로 산술 변환되어도 상관없는 실인수만 사용해야 한다.

명시적 구체화

함수의 호출부를 보고 컴파일러가 템플릿 함수를 알아서 만드는 것을 암시적 구체화라고 한다. 개발자가 원하는 타입으로 함수를 호출하기만 하면 나머지는 컴파일러가 다 알아서 하며 호출하지 않는 타입에 대해서는 구체화하지 않는다. 만약 특정 타입에 대한 템플릿 함수를 강제로 만들고 싶다면 이때는 명시적 구체화(Explicit Instantiation)를 하는데 이는 지정한 타입에 대해 함수를 생성하도록 컴파일러에게 지시하는 것이다. 예를 들어 float 타입을 교환하는 함수를 생성하고 싶다면 다음 명령을 사용한다.

template void Swap<float>(float, float);

명시적 구체화 명령의 표기는 일단 키워드 template가 앞에 오고 함수 이름 다음에 생성하고 싶은 타입을 < > 괄호안에 적는다. 이 선언에 의해 float형을 인수로 취하는 Swap(float, float) 함수가 만들어진다. 당연한 얘기겠지만 템플릿이 어떤 모양인지를 알아야 컴파일러가 이런 함수를 만들 수 있으므로 명시적 구체화 명령은 템플릿 선언보다 뒤에 와야 한다.
이 함수가 당장 필요치 않더라도 일단 만들어 놓고 싶다면 명시적 구체화로 강제 생성을 지시할 수 있다. 예를 들어 지금 작성하는 소스에서는 이 함수가 필요치 않지만 컴파일된 라이브러리로 배포하고 싶다면 명시적 구체화를 할 필요가 있다. 그러나 실제 상황에서 이런 경우는 거의 발생하지 않는데 왜냐하면 함수 템플릿 정의문은 보통 헤더 파일에 작성하며 헤더 파일을 배포하기 때문이다. 라이브러리를 사용하는 측에서 헤더 파일을 인클루드하고 Swap(float, float)를 호출하면 그때 컴파일러가 이 함수를 구체화할 것이므로 문제가 되지 않을 것이다.
다만 함수의 내용을 숨기고 싶을 때는 함수 템플릿을 공개할 수 없으므로 이럴 때는 명시적 구체화로 자주 사용할만한 타입에 대해 일련의 함수 집합을 미리 생성해 놓는다. 이 라이브러리의 사용자는 개발자가 명시적으로 구체화해 놓은 함수만 사용할 수 있을 것이다. 명시적 구체화는 컴파일 속도에도 긍정적인 효과가 있는데 미리 필요한 함수를 생성해 놓으면 컴파일러가 어떤 함수를 생성할 것인지를 판단하는 시간을 조금 절약할 수 있다.


31-1-다.동일한 알고리즘 조건

함수 템플릿은 코드는 동일하고 타입만 다른 함수의 집합을 정의한다. 즉, 템플릿으로 정의할 수 있는 함수들은 문제를 푸는 알고리즘이 동일해야 하며 알고리즘이 다른 함수는 템플릿의 일원이 될 수 없다. 이런 함수들의 집합을 몇 개 더 구경해 보도록 하자.

  : TemplateFunc
#include <Turboc.h>

template <typename T>
T Max(T a, T b)
{
     return (a > b) ? a:b;
}

template <typename T>
T Add(T a, T b)
{
     return a+b;
}

template <typename T>
T Abs(T a, T b)
{
     return (a > 0) ? a:-a;
}

void main()
{
     int a=1,b=2;
     double c=3.4,d=5.6;
     printf("더 큰 정수 = %d\n",Max(a,b));
     printf("더 큰 실수 = %f\n",Max(c,d));
}

두 값 중 큰 값을 찾는 Max, 두 값의 합을 구하는 Add, 절대값을 찾는 Abs 함수들이 템플릿으로 정의되어 있다. 이 함수들은 인수로 전달된 임의의 타입에 대해 동작할 수 있으며 호출부에서는 실인수의 타입을 보고 적절한 함수를 구체화하여 호출한다.
예제의 세 함수 템플릿들을 보면 값을 비교, 연산하고 선택하는 알고리즘이 타입과 상관없이 항상 동일하다는 것을 알 수 있다. 달라질 수 있는 것은 오로지 타입뿐이므로 이런 함수들이 템플릿으로 통합될 수 있는 것이다. 만약 알고리즘이 동일하지 않다면, 즉 함수의 본체가 완전히 달라야 한다면 이 함수들은 같은 템플릿으로 통합될 수 없다. 예를 들어 두 값을 교환하는 Swap 함수 템플릿의 경우 임의의 타입에 대해 잘 동작하지만 배열에 대해서는 동작하지 않는다. 만약 다음과 같이 배열을 가리키는 포인터를 두 개 선언하고 이 포인터를 Swap 함수로 전달했다고 해 보자.

int a[]={1,2,3},b[]={4,5,6};
int *pa=a,*pb=b;
Swap(pa, pb);
// Swap(a, b);

Swap(pa, pb)는 일단 정상적으로 동작하는 것처럼 보인다. 그러나 이는 배열을 가리키는 포인터만 교환한 것이지 배열 자체가 교환된 것은 아니다. Swap(a, b) 호출로 배열 자체를 교환하려고 시도하면 포인터 상수인 배열명을 변경할 수 없다는 에러로 처리된다. 두 배열의 타입과 크기가 일치하더라도 배열을 교환하는 알고리즘은 단순 타입이나 구조체를 교환하는 것과는 다르다. 배열끼리는 대입되지 않으므로 배열의 요소들을 일대일로 교환해야 하며 배열의 크기가 가변적이므로 길이에 대한 별도의 정보를 더 전달해야 한다.
두 배열의 내용을 통째로 교환하려면 별도의 함수를 만들어야 하는데 다양한 배열 요소에 대해 동작하려면 요소의 타입별로 일련의 함수를 만들어야 할 것이다. 이때도 요소의 타입만 달라지므로 템플릿을 사용할 수 있다. 다음 예제는 배열을 교환하는 함수 템플릿을 정의하는데 임의의 T형을 요소로 가지는 num길이의 두 배열을 메모리 복사를 통해 교환한다.

  : SwapArray
#include <Turboc.h>

template <class T>
void SwapArray(T *a, T *b,int num)
{
     void *t;

     t=malloc(num*sizeof(T));
     memcpy(t,a,num*sizeof(T));
     memcpy(a,b,num*sizeof(T));
     memcpy(b,t,num*sizeof(T));
     free(t);
}

void main()
{
     int a[]={1,2,3},b[]={4,5,6};
     char c[]="문자열",d[]="string";
     SwapArray(a,b,sizeof(a)/sizeof(a[0]));
     printf("before c=%s,d=%s\n",c,d);
     SwapArray(c,d,sizeof(c)/sizeof(c[0]));
     printf("after c=%s,d=%s\n",c,d);
}

앞에서 만들었던 SwapVoid와 상당히 유사한데 메모리의 길이를 인수로 전달받는 것이 아니라 요소의 개수를 전달받는다는 점이 다르다. main에서 크기 3의 정수형 배열과 크기 7의 문자형 배열에 대해 교환을 했으므로 SwapArray는 두 가지 버전으로 구체화될 것이다. 실행 결과는 다음과 같다.

before c=문자열,d=string
after c=string,d=문자열

보다시피 배열을 교환하는 알고리즘은 단순 타입을 교환하는 알고리즘과 완전히 틀리고 필요한 인수 목록도 다르기 때문에 하나의 함수 템플릿으로 통합될 수 없으며 따로 템플릿을 구성해야 한다. 이 예제에서는 배열을 교환하는 함수 템플릿에 SwapArray라는 이름을 사용했는데 인수 목록이 달라 오버로딩이 가능하므로 Swap이라는 이름을 같이 써도 상관없다. 즉 템플릿끼리도 오버로딩은 가능하다.

31-1-라.임의 타입 지원 조건

함수 템플릿의 본체 코드는 임의의 타입에 대해서도 동일하게 동작해야 하므로 타입에 종속적인 코드는 사용할 수 없다. 기본 타입에 대해 이미 오버로딩되어 있는 +, - 등의 연산자를 사용하거나 cout과 같이 피연산자의 타입을 스스로 판별할 수 있는 코드만 사용해야 한다. printf 함수처럼 타입에 따라 서식을 미리 결정해야 하는 함수는 함수 템플릿에서 쓰지 않는 것이 바람직하다. 다음 함수를 보자.

template <typename T>
void PrintValue(T value)
{
     printf("value is %d\n",value);
}

템플릿으로 되어 있어서 임의의 타입을 받을 수는 있지만 출력 코드에서 %d 서식을 사용하고 있으므로 사실상 정수 호환 타입만 출력할 수 있다. 정수 이외의 타입을 %d 서식으로 출력하면 어떤 값이 출력될 지 알 수 없다. 좀 더 범용성을 높이려면 cout으로 출력하는 것이 유리한데 cout은 임의 타입을 출력할 수 있고 사용자 정의 타입도 << 연산자만 적절하게 오버로딩되어 있으면 똑같은 방법으로 출력할 수 있다.
앞 예제의 Add 함수 템플릿은 + 연산자로 피연산자를 더하는데 + 는 대부분의 기본 타입에 대해 오버로딩되어 있으므로 아무 타입이나 잘 더할 수 있을 것 같다. 그러나 이 연산자를 쓸 수 있는 정수, 실수, 문자형 등의 수치형에 대해서만 사용할 수 있을 뿐이다. 문자열(char *)을 더할 때는 포인터끼리 더할 수 없으므로 strcat 함수를 사용해야 한다. 더하는 알고리즘이 완전히 다르므로 Add 함수를 사용할 수 없다. 물론 + 연산자를 오버로딩하고 있는 문자열 객체라면 이 함수로 연결할 수 있을 것이다.
템플릿 본체에 사용된 모든 코드와 호환되는 타입에 대해서만 구체화할 수 있다. 두 값 중 큰 값을 찾는 Max 템플릿은 단순히 > 연산자와 삼항 조건 연산자만으로 값의 대소를 판단할 뿐이다. 지극히 간단해서 별다른 제약이 없을 것 같지만 이 간단한 템플릿도 안 통하는 경우가 있다. 다음 예제를 보자.

  : MaxObject
#include <Turboc.h>

template <typename T>
T Max(T a, T b)
{
     return (a > b) ? a:b;
}

struct S {
     int i;
     S(int ai) : i(ai) { }
     //operator >(S &Other) { return i > Other.i; }
};

void main()
{
     int i1=3,i2=4;
     double d1=1.2,d2=3.4;
     S s1(1),s2(2);

     Max(i1,i2);
     Max(d1,d2);
     Max(s1,s2);
}

Max는 두 값 중 큰 값을 리턴하는데 정수나 실수에 대해서는 잘 동작한다. 그러나 구조체 S에 대해서는 동작하지 않는데 구조체끼리는 > 연산자로 비교할 수 없기 때문이다. 구조체에 > 연산자를 오버로딩해 놓으면 이때는 S객체끼리 대소 비교가 가능해 지므로 Max(s1, s2) 호출도 잘 컴파일된다. S가 > 연산자를 오버로딩하지 않더라도 이 호출을 주석으로 처리하면 Max(S, S)가 구체화되지 않으므로 아무 일 없다는 듯 잘 컴파일된다.
컴파일러는 구체화된 템플릿 함수에 대해서만 에러 체크를 할 뿐이지 템플릿 자체에 대해서는 상세한 점검을 할 수 없다. 템플릿을 정의할 때는 어떤 타입이 전달될지 모르므로 컴파일러는 템플릿의 모양만 기억해 둘 뿐 구문상의 에러 체크를 하지 않는다. 심지어 다음과 같은 템플릿도 아무 문제없이 컴파일된다.

template <typename T>
void Some(T arg)
{
     arg.ar[34]=arg.next;
     arg.next->value=1234;
}

이 템플릿은 인수로 전달받은 arg가 구조체이고 이 구조체안에 크기가 최소한 35이상인 ar 배열이 멤버로 포함되어 있으며 next는 다른 구조체를 가리키는 포인터이고 next가 가리키는 구조체의 value 멤버는 정수형 변수라는 것을 가정하고 있다. 이 가정이 맞는지 아닌지는 실제로 전달되는 T가 어떤 타입인가에 따라 달라지는 것이므로 컴파일러는 구체화될 때까지는 에러 체크를 보류할 수밖에 없는 것이다. 오타가 있거나 if = for + while; 같은 말도 안되는 구문까지도 일단은 컴파일된다.
템플릿은 인수로 전달된 임의의 타입에 대해 동작할 수 있는 함수의 형틀이지만 그 본체에서는 전달될만한 타입을 모두 지원하는 범용적인 코드만 작성해야 한다. 또는 템플릿으로 전달된 타입이 해당 템플릿의 본체 코드의 요구 조건을 모두 만족해야 한다. 그렇지 않을 경우 잘 사용하던 템플릿도 특정 타입에 대해 구체화했을 때 갑자기 에러가 발생할 수도 있다. 다음 예제로 테스트해 보자.

  : SwapPerson
#include <Turboc.h>

template <typename T>
void Swap(T &a, T &b)
{
     T t;
     t=a;a=b;b=t;
}

class Person
{
private:
     char *Name;
     int Age;

public:
     Person() {
          Name=new char[1];
          Name[0]=NULL;
          Age=0;
     }
     Person(const char *aName, int aAge) {
          Name=new char[strlen(aName)+1];
          strcpy(Name,aName);
          Age=aAge;
     }
     Person(const Person &Other) {
          Name=new char[strlen(Other.Name)+1];
          strcpy(Name,Other.Name);
          Age=Other.Age;
     }
/*
     Person &operator =(const Person &Other)   {
          if (this != &Other) {
              delete [] Name;
              Name=new char[strlen(Other.Name)+1];
              strcpy(Name,Other.Name);
              Age=Other.Age;
          }
          return *this;
     }
//*/
     virtual ~Person() {
          delete [] Name;
     }
     virtual void OutPerson() {
          printf("이름 : %s 나이 : %d\n",Name,Age);
     }
};

void main()
{
     Person A("이승만",10);
     Person B("박정희",20);
     A.OutPerson();B.OutPerson();
     Swap(A,B);
     A.OutPerson();B.OutPerson();
}

예제의 선두에는 앞에서 만들어서 이미 테스트가 완료된 Swap 템플릿이 정의되어 있다. 변수 교환 알고리즘이 워낙 간단해서 별 문제가 없을 것 같지만 Person 객체에 대해서는 제대로 동작하지 않으며 끝낼 때 다운된다. Swap 템플릿은 변수값 교환을 위해 세 번 대입을 하므로 이 코드가 이상없이 동작하려면 대입이 가능해야 하고 대입에 의해 별다른 문제가 없어야 한다. 그러나 예제의 Person 클래스는 대입 연산자를 제대로 정의하지 않아 템플릿의 코드와는 맞지 않다.
Person 객체 A와 B에 대해 Swap(A,B)를 호출했을 때 어떤 일들이 벌어지는지 상상해 보자. 컴파일러는 함수 호출부의 타입을 보고 Swap(Person, Person) 함수를 구체화하여 형식 인수 a, b로 실인수 A, B를 전달한다. Person에는 복사 생성자가 정의되어 있으므로 여기까지는 아주 정상적이다. 그러나  교환을 위해 a를 t에 대입하는 순간 t는 얕은 복사에 의해 a와 버퍼를 공유하게 되며 이 상태에서 t의 값을 b가 대입받았다. Swap 함수가 종료될 때 a는 b의 값을 무사히 대입받았지만 b와 t가 버퍼를 공유하며 지역 객체 t가 파괴되면서 b의 버퍼를 정리해 버린다. main으로 돌아 왔을 때 실인수 B의 버퍼가 이중 해제되므로 다운되는 것이다.
틀린 코드임에도 불구하고 컴파일 에러가 발생하지 않는 이유는 대입 연산자는 디폴트가 있으므로 일단 대입은 가능하기 때문이다. 디폴트 대입 연산자에 의한 얕은 복사가 문제의 원인이었으므로 깊은 복사를 하는 대입 연산자를 정의하면 문제가 해결된다. 예제의 주석으로 묶여있는 대입 연산자를 풀어 보면 아무 이상없이 잘 동작할 것이다.
템플릿은 지금 당장 잘 컴파일되고 이상없이 동작하는 것처럼 보이더라도 타입이 바뀌면 어떻게 될지 장담할 수 없다. 완벽할 수는 없겠지만 템플릿은 가급적이면 많은 타입을 지원할 수 있도록 범용적인 코드를 작성해야 하며 템플릿의 인수로 사용될 클래스는 템플릿 본체가 요구하는 모든 기능을 지원해야 한다. 가장 이상적인 타입은 기본 타입인 int이므로 int와 똑같은 방식으로 동작하는 클래스를 만든다면 거의 안전하다. 왜 클래스가 완전한 타입 흉내를 내려고 그토록 몸부림을 치는지 이해할 수 있겠는가?

31-1-마.특수화

같은 템플릿으로부터 만들어진 함수는 타입만 제외하고 동일한 본체를 가지므로 동작도 동일하다. 만약 특정 타입에 대해서만 다르게 동작하도록 하고 싶다면 이때는 특수화(Specialization)라는 기법을 사용한다. 예를 들어 Swap 함수를 실수에 대해 적용할 때는 값을 전부 교환하지 말고 정수부만 교환하고 싶다고 하자. 이럴 때는 double 형에 대해서 특수한 함수를 하나 만들면 된다.

  : Specialization
#include <Turboc.h>

template <class T>
void Swap(T &a, T &b)
{
     T t;
     t=a;a=b;b=t;
}

template <> void Swap<double>(double &a, double &b)
{
     int i,j;

     i=(int)a;
     j=(int)b;
     a=a-i+j;
     b=b-j+i;
}

void main()
{
     double a=1.2,b=3.4;
     printf("before a=%g, b=%g\n",a,b);
     Swap(a,b);
     printf("after a=%g, b=%g\n",a,b);
}

Swap 함수 템플릿을 정의해 두고 double형에 대해서 특별한 Swap 함수를 따로 정의했다. double에 대해 특수화된 Swap 함수의 본체는 정수부만 교환하는 고유한 코드를 가진다. main 에서는 두 개의 실수를 Swap 함수로 교환했는데 실행 결과는 다음과 같다.

before a=1.2, b=3.4
after a=3.2, b=1.4

만약 double에 대한 특수화를 하지 않으면 일반적인 Swap 함수가 호출되어 소수부, 실수부가 같이 바뀔 것이다. 컴파일러는 템플릿 함수 호출 구문이 있을 때 항상 템플릿의 정의보다 특수화된 정의에 우선권을 주므로 동일한 이름의 템플릿과 특수화 함수가 존재하면 특수화된 함수가 호출된다. 특수화 함수를 표기하는 방법은 여러 가지가 있다.

① template <> void Swap<double>(double &a, double &b)
② template <> void Swap<>(double &a, double &b)
③ template <> void Swap(double &a, double &b)
④ void Swap<double>(double &a, double &b)
⑤ void Swap<>(double &a, double &b)
⑥ void Swap(double &a, double &b)

특수화된 함수라는 것을 표시하기 위해 template <> 로 시작하는데 <>가 없으면 명시적 구체화 구문이 되므로 잘 구분해야 한다. 함수 이름 뒤에는 어떤 타입에 대한 특수화 함수인지 <> 괄호와 특수화된 타입 이름을 밝힌다. ①번 표기법이 가장 완전한 형태이되 좀 더 간략한 표기법도 쓸 수 있다. 어떤 타입에 대해 특수화되었는지는 어차피 인수의 타입으로도 알 수 있으므로 함수명 다음의 <> 괄호는 생략 가능하며 <>만 남겨 두고 타입만 생략하는 것도 가능하다. 단, 템플릿 인수가 리턴 타입이나 내부 지역변수로 사용될 때는 ①번 타입만 가능하다.
또한 ④번처럼 함수명 다음에 <> 괄호가 있다면 이 표기로부터 함수 템플릿에 대한 특수화 함수라는 것을 알 수 있으므로 앞쪽의 template <> 도 생략 가능하다. 이 표기법은 구형 컴파일러들이 주로 사용하던 방법이며 ⑤번처럼 <> 괄호안에 타입명을 생략해도 상관없다. 대부분의 컴파일러가 아직까지도 이 표기법을 지원하고는 있지만 최신 표준에는 이 표기법이 인정되지 않으므로 가급적 사용을 자재해야 한다.
 특수화 함수를 표기하는 방법이 왜 이렇게 많은가 하면 템플릿이라는 기능이 처음부터 표준에 의해 정립된 것이 아니라 각 컴파일러 제작사들에 의해 비공식적으로 발전해 오다가 비교적 최근에 표준으로 채택되었기 때문이다. 표준이 제정되었다고 해서 이전에 사용하던 형식을 무시할 수는 없기 때문에 이런 많은 표기법들이 난무하는 상황이 되었는데 이런 면을 보면 표준이 얼마나 중요한가를 알 수 있다. 표준 제정이 늦어지면 변종들이 생겨 여러 사람들이 피곤해진다.
마지막 ⑥번 형식은 특수화 함수가 아니라 그냥 일반 함수 Swap을 정의하는 것이다. 이렇게 일반 함수를 정의해도 일단은 목적을 이룰 수 있지만 우선 순위의 문제가 있어 바람직하지 않다. 함수 템플릿과 특수화된 함수, 그리고 일반 함수가 동시에 존재할 경우 어떤 함수를 우선적으로 선택할 것인가는 컴파일러마다 다르다. 만약 일반 함수가 템플릿 함수보다 우선 순위가 늦다면 지정한 타입에 대해 특수한 처리를 할 수 없을 것이다.



31-2.클래스 템플릿

31-2-가.타입만 다른 클래스들

클래스 템플릿은 함수 템플릿과 비슷하되 찍어내는 대상이 클래스라는 것만 다르다. 구조나 구현 알고리즘은 동일하되 멤버들의 타입만 다를 경우 클래스를 일일이 따로 만드는 대신 템플릿을 정의한 후 템플릿으로부터 클래스를 만들 수 있다. 실용적 가치는 별로 없지만 화면상의 특정 좌표에 출력될 값에 대한 정보를 표현하는 클래스를 만들어 보자. 정보의 타입에 따라 값을 표현하는 멤버의 타입이 달라지므로 타입에 따라 클래스를 일일이 만들어야 한다.

class PosValueInt
{
private:
     int x,y;
     int value;
public:
     PosValue(int ax, int ay, int av) : x(ax),y(ay),value(av) { }
     void OutValue();
};

class PosValueChar
{
private:
     int x,y;
     char value;
public:
     PosValue(int ax, int ay, char av) : x(ax),y(ay),value(av) { }
     void OutValue();
};

class PosValueDouble
{
private:
     int x,y;
     double value;
public:
     PosValue(int ax, int ay, double av) : x(ax),y(ay),value(av) { }
     void OutValue();
};

좌표값 x, y는 모든 클래스에서 int형이며 값을 표현하는 value 멤버의 타입만 달라진다. 클래스는 함수에서와 같은 오버로딩이 지원되지 않으므로 이름을 모두 다르게 작성해야 하며 value의 타입에 따라 생성자의 원형도 각기 다르다. 결국 실제로 다른 부분은 value의 타입뿐이며 나머지는 모두 동일하므로 이 클래스들을 하나의 템플릿으로 통합할 수 있다.

  : PosValueTemp
#include <Turboc.h>
#include <iostream>
using namespace std;

template <typename T>
class PosValue
{
private:
     int x,y;
     T value;
public:
     PosValue(int ax, int ay, T av) : x(ax),y(ay),value(av) { }
     void OutValue();
};

template <typename T>
void PosValue<T>::OutValue()
{
     gotoxy(x,y);
     cout << value << endl;
}

void main()
{
     PosValue<int> iv(1,1,2);
     PosValue<char> cv(5,1,'C');
     PosValue<double> dv(30,2,3.14);
     iv.OutValue();
     cv.OutValue();
     dv.OutValue();
}

클래스 선언문앞에 template <typename T>를 붙이고 타입에 종속적인 부분에만 T를 사용하면 된다. 예제의 PosValue 클래스는 템플릿 인수로 전달받은 타입 T를 value의 타입으로 선언하였고 생성자의 세 번째 인수도 T형이 된다. 이렇게 정의된 클래스 타입의 객체를 생성할 때 클래스 이름 다음의 < > 괄호안에 원하는 타입을 밝혀야 한다. 클래스 템플릿으로부터 만들어지는 클래스를 템플릿 클래스라고 하는데 템플릿 클래스의 타입명에는 < > 괄호가 항상 따라 다닌다. value가 int형인 클래스의 이름은 PosValue<int>이고 value가 char형인 클래스의 이름은 PosValue<char>이다.
단 예외적으로 생성자의 이름은 클래스의 이름을 따라가지만 클래스 템플릿의 경우 템플릿 이름을 사용해도 상관없다. <T> 괄호가 있거나 없거나 상관없다는 얘기인데 위 예제의 PosValue 생성자를 PosValue<T>(int ax, int ay, T av)로 정의할 수도 있다. 보통은 생성자에 대해서는 <T>를 붙이지 않는다.
클래스 템플릿의 멤버 함수를 선언문 외부에서 작성할 때는 템플릿에 속한 멤버 함수임을 밝히기 위해 소속 클래스의 이름에도 <T>를 붙여야 하며 T가 템플릿 인수임을 명시하기 위해 template <typename T>가 먼저 와야 한다. OutValue 멤버 함수는 PosValue<T> 클래스 소속이며 이때 T는 템플릿 인수 목록으로 전달된 타입의 이름이다. 함수 본체 내에서는 T를 언제든지 참조할 수 있다. 클래스 선언문 내부에서 인라인으로 함수를 선언할 때는 클래스 선언문앞에 T에 대한 설명이 있으므로 이렇게 하지 않아도 상관없다.

template <typename T>
class PosValue
{
     ....
     void OutValue() {
          gotoxy(x,y);
          cout << value << endl;
     }
};

템플릿 클래스로부터 객체를 선언할 때는 템플릿 이름 다음에 < >괄호를 쓰고 괄호안에 T로 전달될 타입의 이름을 명시해야 한다. PosValue<int>는 int타입의 value를 멤버로 가지는 PosValue 템플릿 클래스를 의미하며 PosValue<double> 클래스의 value는 double 타입이 된다. 템플릿 클래스의 이름에는 타입이 분명히 명시되어야 한다. PosValue라는 명칭은 어디까지나 템플릿의 이름일 뿐이므로 이 이름으로부터 객체를 생성할 수는 없다.
컴파일러는 객체 선언문에 있는 초기값의 타입으로부터 어떤 타입에 대한 클래스를 원하는지 알 수 있을 것도 같다. 예를 들어 PosValue iv(1,1,2)라고 쓰면 제일 마지막 인수가 int형 상수이므로 PosValue<int> 타입이라고 유추 가능할 것이다. 그러나 생성자가 오버로딩되어 있을 경우 이 정보만으로는 원하는 타입을 정확하게 판단하기 어렵다. 또한 생성자를 호출하기 전에 객체를 위한 메모리를 할당해야 하는데 이 시점에서 생성할 객체의 크기를 먼저 계산할 수 있어야 하므로 클래스 이름에 타입이 명시되어야 한다.
함수에서와 마찬가지로 클래스 템플릿도 단순한 선언에 불과하며 컴파일러는 이 템플릿의 모양을 기억해 두었다가 객체가 생성될 때 전달된 타입에 맞는 클래스 정의를 구체화한다. 만약 클래스 템플릿 선언만 있고 객체를 생성하지 않는다면 템플릿은 무시된다. main에서 int, char, double 타입의 PosValue 객체를 각각 선언했는데 이 선언문에 의해 세 개의 클래스가 구체화될 것이다. 확인을 위해 세 개의 객체를 만든 후 OutValue 함수를 호출하여 각 좌표에 값을 출력해 보았다.
템플릿으로부터 만들어지는 클래스도 분명히 클래스이며 일반적인 클래스와 전혀 다를 바가 없다. 템플릿 클래스로부터 상속하는 것도 가능하며 문법도 동일하되 기반 클래스의 이름에 < > 괄호가 사용되는 차이밖에 없다. 다음 클래스는 PosValue<int>로부터 새로운 클래스를 파생한다.

class PosValue2 : public PosValue<int> { ... }

템플릿 클래스가 다른 클래스의 기반 클래스로 사용되면 컴파일러는 클래스를 즉시 구체화한다. 설사 이 클래스의 인스턴스 선언문이 없더라도 말이다. 템플릿으로부터 만들어지지 않은 일반 클래스의 특정 멤버 함수만 템플릿으로 선언하는 것도 가능하다. 멤버 함수도 분명히 함수이므로 타입에 따라 여러 벌이 필요하다면 원하는 함수 하나만 함수 템플릿으로 만들면 된다. 다음 예제는 그 예를 보여 준다.

  : TempMember
#include <Turboc.h>
#include <iostream>
using namespace std;

class Some
{
private:
     int mem;

public:
     Some(int m) : mem(m) { }
     template <typename T>
     void memfunc(T a) {
          cout << "템플릿 인수 = " << a << ", mem = " << mem << endl;
     }
};

void main()
{
     Some s(9999);

     s.memfunc(1234);
     s.memfunc(1.2345);
     s.memfunc("string");
}

Some 클래스에는 함수 템플릿이 하나 포함되어 있으며 이 함수는 임의 타입 T형의 변수 a를 인수로 전달받아 그 값을 화면으로 출력한다. 실제 어떤 멤버 함수가 호출되는가에 따라 클래스 Some의 멤버 함수 개수가 결정될 것이다.


31-2-나.템플릿의 위치

클래스 템플릿 선언문은 반드시 사용하기 전에 와야 한다. PosValueTemp 예제에서 보다시피 main 함수보다 템플릿 선언이 더 앞에 있는데 이 순서가 바뀌면 main에서 참조하는 PosValue<int>, PosValue<char>가 무엇을 의미하는지 모르므로 에러로 처리될 것이다. 단, 템플릿 클래스의 멤버 함수 본체 정의문은 앞쪽에 이미 소속과 원형이 선언되어 있으므로 main 함수보다 뒤에 있어도 상관없다.
예제 수준에서는 한 파일안에 클래스 선언과 멤버 함수의 정의, 그리고 이 클래스를 사용하는 테스트 코드까지 모두 같이 작성하는 것이 편리하지만 실제 프로젝트에서는 클래스별로 모듈을 구성하는 것이 일반적이다. 클래스 템플릿의 경우도 마찬가지로 별도의 모듈을 작성할 수 있는데 이때 템플릿 선언문과 멤버 함수의 정의까지 모두 헤더 파일에 작성되어야 한다.
멤버 함수를 정의하는 함수 템플릿은 실제로 함수의 본체를 만드는 것이 아니므로 구현 파일에 작성해서는 안된다. 만약 PosValue 클래스 템플릿은 PosValue.h에서 선언하고 이 클래스에 속한 멤버 함수에 대한 정의는 PosValue.cpp에 다음과 같이 따로 작성한다고 해 보자.

#include "PosValue.h"

template <typename T>
void PosValue<T>::OutValue()
{
     gotoxy(x,y);
     cout << value << endl;
}

이렇게 되면 OutValue 함수는 PosValue.cpp 안에서만 알려지므로 다른 모듈에 있는 main 함수에서는 OutValue가 정의되지 않은 것으로 인식되어 에러로 처리된다. 일반 함수는 컴파일시에 원형만 선언하면 컴파일 가능하고 링크할 때 바인딩되는데 비해 템플릿은 컴파일할 때 완벽하게 구체화되어야 하므로 같은 번역 단위안에 선언이 있어야 한다. C/C++ 컴파일러의 번역 단위는 Cpp 파일 + 포함된 헤더 - 조건부 컴파일로 제외된 부분이다.
템플릿은 만들고자 하는 클래스와 멤버 함수의 모양을 컴파일러에게 알리기만 할 뿐이지 그 자체가 코드를 생성하는 것은 아니며 따라서 외부로 알려지지 않는다. 클래스 템플릿은 헤더 파일에 작성하는 것이 원칙적이며 실제 코드를 생성하는 것이 아니므로 설사 이 헤더 파일을 여러 모듈에서 인클루드하더라도 중복 정의되었다는 에러가 발생하지는 않는다. 한 모듈에서 같은 이름에 대해 #define을 두 번 하면 안되지만 #define 문이 있는 헤더를 각 모듈에서 인클루드해도 문제가 안되는 것과 같다.
그러나 헤더 파일에 클래스 템플릿을 두게 되면 최종 사용자에게 이 클래스의 코드를 숨길 수 없다는 단점이 있다. 기술적으로 중요한 사항을 담고 있는 클래스의 소스가 누출될 수 있는 보안상의 문제가 있는 것이다. 그래서 최신 C++ 표준은 cpp 파일에 클래스 템플릿의 멤버 함수를 정의할 수 있는 export 키워드를 도입하고 이 키워드를 사용하면 구현 파일에 정의된 멤버 함수가 외부로도 알려지도록 한다. 템플릿 선언앞에 export를 붙이면 된다.

export template <typename T>
void PosValue<T>::OutValue() { ... }

그러나 이 키워드는 몇몇 시험적인 컴파일러들만 지원하고 있을 뿐이며 비주얼 C++, gcc를 포함한 대부분의 컴파일러에서 아직 지원하지 않는다. 표준으로 채택되었음에도 불구하고 지원하지 못하는 컴파일러가 많은 이유는 이 키워드가 전통적인 모듈 분할 방식의 컴파일러와는 잘 맞지 않기 때문이다. C/C++ 컴파일러는 번역 단위별로 컴파일하여 링크할 때 합치는 방식을 사용하는데 export로 지정된 함수에 대해서는 모든 번역 단위에 대해서도 그 정의를 알려야 하므로 근본적인 컴파일 방식을 바꾸기 전에는 지원하기가 대단히 어렵다. 안타깝게도 이 키워드는 당분간은 쓸 수 없으며 그래서 템플릿 라이브러리들은 거의 대부분 소스가 공개되어 있다.


31-2-다.비타입 인수

템플릿의 인수 목록에 전달되는 것은 통상 타입이다. 알고리즘은 같되 타입만 다른 함수나 클래스를 작성하고 싶을 때 템플릿을 사용한다. 그러나 타입이 아닌 상수를 템플릿 인수로 전달할 수 있는데 이를 비타입 인수(Nontype Argument)라고 한다. 다음 예제의 Array 클래스는 임의의 타입에 대한 배열을 정의하고 배열 요소의 값을 변경하거나 읽는 기능을 제공한다. 임의의 타입에 대한 배열을 만들기 위해 타입 이름을 템플릿 인수로 전달받으며 배열의 크기 지정을 위해 정수 상수를 전달받는다.

  : NonTypeArgument
#include <Turboc.h>

template <typename T, int N>
class Array
{
private:
     T ar[N];
public:
     void SetAt(int n,T v) { if (n < N && n >=0) ar[n]=v; }
     T GetAt(int n) { return (n < N && n >=0 ? ar[n]:0); }
};

void main()
{
     Array<int,5> ari;
     ari.SetAt(1,1234);
     ari.SetAt(1000,5678);
     printf("%d\n",ari.GetAt(1));
     printf("%d\n",ari.GetAt(5));
}

기능상 단순 배열과 유사하지만 좀 더 안전한 액세스를 지원하는데 요소값을 읽거나 쓰는 Get(Set)At 함수가 전달된 첨자의 범위를 점검하므로 실수로 범위 바깥을 액세스해도 치명적인 에러를 발생시키지 않는다. 이외에 확장하기에 따라서는 얼마든지 다양한 기능을 더 넣을 수 있을 것이다. main에서는 크기 5의 정수형 배열 ari를 선언하고 배열에 값이 잘 기억되는지 확인해 보았으며 엉뚱한 첨자에 대해 방어를 제대로 하는지도 테스트해 보았다. 1234와 0이라는 값이 출력되면 정상적으로 동작하는 것이다.
Array 클래스의 T형 배열 멤버 ar은 크기 N을 가지는데 이 크기는 객체를 선언할 때 인수로 주어지는 정수 상수이다. 템플릿으로 전달되는 인수가 타입이 아니므로 비타입 인수라고 한다. 컴파일러는 ari 선언문에 명시되어 있는 타입 Array<int,5>로부터 다음과 같은 클래스를 구체화할 것이다.

class Array
{
private:
     int ar[5];
public:
     void SetAt(int n,int v) { if (n < 5 && n >=0) ar[n]=v; }
     int GetAt(int n) { return (n < 5 && n >=0 ? ar[n]:0); }
};

임의 타입에 대해 임의 크기까지를 지원하는 안전 배열 클래스를 만들고 싶다면 이런 비타입 인수를 사용할 수 있다. 물론 임의 크기를 지원하는 더 좋은 방법은 생성자의 인수로 전달되는 값으로 동적 할당을 하는 것이다. 포인터형 멤버 변수를 선언하면 필요한만큼 할당할 수 있으며 원할 경우 실행중에라도 크기를 마음대로 바꿀 수도 있어 굉장히 신축적이다.
그러나 알다시피 동적할당은 생성자와 파괴자를 요구하고 정확하게 동작하기 위해서는 복사 생성자, 대입 연산자가 반드시 적절하게 정의되어야 하며 상속 관계를 고려하면 대부분의 멤버 함수들은 가상 함수가 되어야 한다. 좋기는 하지만 코드가 져야 할 부담이 너무 많은 것이다. 이럴 때 동적 할당 대신 필요한 크기만큼의 요소를 정적으로 가지는 클래스를 만들어 쓰면 속도도 빠르고 위험하지도 않으며 무엇보다 단순해서 좋다. 예제의 Array 템플릿은 생성자, 파괴자, 복사 생성자, 대입 연산자 중 어떤 것도 필요없다. 아니, 필요는 하지만 컴파일러가 디폴트로 만드는 것만으로도 충분하고 안전하다.
실행중에 크기를 결정하기 힘든 중요한 상수에 대해서는 이런 식으로 템플릿과 비타입 인수를 사용할 수 있다. 크기가 다른 객체를 선언할 때마다 클래스가 구체화된다는 점에서 낭비가 조금 있기는 하지만 말이다. 클래스 선언문의 템플릿 인수가 다르면 객체의 타입도 달라진다. 컴파일러는 완전히 같지 않은 템플릿 인수에 대해서는 개별적으로 구체화를 하기 때문이다. 심지어 멤버 함수들도 전부 따로 만들어진다. 다음 코드를 보자.

Array<int,5> ari;
Array<int,5> ari2;
Array<int,6> ari3;

ari=ari2;
ari=ari3;        // 에러

ari와 ari2는 같은 타입이므로 서로 대입 가능하지만 ari3를 ari에 대입하는 것은 에러이다. 왜냐하면 ari는 Array<int,5> 타입이고 ari3는 Array<int,6> 타입이기 때문이다. 물론 Array<double,5> 타입도 Array<int, 5>와 호환되지 않는 다른 타입이다. Array<int,6>은 다음과 같이 구체화되며 Array<int,5>와는 크기도 다르고 동작도 다르다.

class Array
{
private:
     int ar[6];
public:
     void SetAt(int n,int v) { if (n < 6 && n >=0) ar[n]=v; }
     int GetAt(int n) { return (n < 6 && n >=0 ? ar[n]:0); }
};

클래스 선언문의 비타입 인수는 반드시 상수여야 하며 실행중에 값이 결정되는 변수는 인수로 사용할 수 없다. 다음 선언문은 에러로 처리된다.

int size=5;
Array<int,size> ari;

size는 변수이며 이 값은 실행중에 수시로 변할 수 있으므로 템플릿의 인수로 사용할 수 없다. 템플릿이란 컴파일러가 인수를 적용하여 컴파일 중에 클래스를 만들어 내는 형틀이므로 모든 정보를 컴파일 중에 알 수 있어야 한다. 실행중에 없던 클래스를 만들어내는 기능이 아니라 컴파일 중에 구체화해야 하므로 변수는 쓸 수 없다. 물론 const int size=5; 로 상수 선언했다면 가능하다.
함수로도 비타입 인수를 전달할 수 있다. 단, 함수의 형식 인수 목록에 어떤 상수가 올 수는 없으므로 비타입 인수는 함수의 본체에서만 사용해야 하며 함수 호출문에 템플릿 인수를 명시적으로 지정해야 한다. 다음 예제는 비타입 인수 N이 지정하는 크기만큼의 지역 배열을 선언하는 함수 템플릿이다.

  : NonTypeArgFunc
#include <Turboc.h>

template <int N>
void func(void)
{
     int ar[N];

     printf("배열 크기=%d\n",N);
}

void main()
{
     func<5>();
     func<8>();
}

main에서 func 함수를 두 번 호출했는데 비타입 인수 N으로 5와 8을 전달했다. 이 두 함수는 지역변수의 선언문이 다르므로 각각 따로 구체화되어야 한다. 비타입 인수는 함수의 인수와는 용도가 다른데 함수의 형식 인수는 실행 시간에 전달되는 변수이므로 배열 선언문 등 상수가 필요한 곳에 사용할 수 없지만 비타입 인수는 구체화될 때 함수 본체에 직접 기입되므로 상수일 수 있다.
func 템플릿은 비타입 인수 N을 요구하므로 func()라고만 호출해서는 지역 배열의 크기를 결정할 수 없으므로 함수를 구체화할 수 없다. 반드시 명시적인 템플릿 인수를 전달해야 한다. 이 예제는 gcc, 비주얼 C++ 7.0이상의 최신 컴파일러에서는 잘 실행되지만 비주얼 C++ 6.0에서는 컴파일되지 않는다. 비주얼 C++ 6.0은 클래스의 비타입 인수는 지원하지만 함수의 비타입 인수는 아직 지원하지 못한다.


31-2-라.디폴트 템플릿 인수

함수의 디폴트 인수는 함수를 호출할 때 생략된 인수에 대해 기본적으로 적용되는 값이다. 클래스 템플릿에도 이와 비슷한 개념인 디폴트 템플릿 인수가 있는데 객체 선언문에서 인수를 생략할 경우 템플릿 선언문에서 지정한 디폴트가 적용된다. 사용하는 표기법이나 주의 사항도 대체로 함수의 경우와 동일하다. 예를 들어 PosValue 클래스 템플릿의 T에 int라는 디폴트 타입을 지정하고 싶다면 다음과 같이 템플릿을 작성한다.

template <typename T=int>
class PosValue
{
     ....

< > 괄호안의 타입 이름 다음에 = 구분자를 쓰고 디폴트로 적용될 타입을 지정한다. 이제 별다른 지정이 없으면 T는 디폴트 타입인 int가 된다. 정수형의 PosValue 객체를 선언할 때 다음과 같이 간단하게 클래스 타입을 지정할 수 있다. 타입 지정없이 빈 < >괄호만 쓰면 된다.

PosValue<> iv(1,1,2);

물론 PosValue<double>과 같이 타입을 분명히 밝히면 디폴트는 무시될 것이다. 디폴트를 그대로 받아들일 경우는 타입 지정을 하지 않으면 되는데 그렇더라도 빈 괄호 < >는 꼭 있어야 한다. 타입을 여러 개 가지는 클래스의 경우 오른쪽 인수부터 차례대로 디폴트를 지정할 수 있으며 객체를 선언할 때는 오른쪽부터 순서대로 생략 가능하다. 이 점도 함수의 디폴트 인수와 같다.
클래스 템플릿에는 디폴트 인수를 줄 수 있지만 함수 템플릿에는 디폴트를 정의할 수 없다. 클래스는 객체를 선언할 때 클래스 타입을 지정하므로 생략 가능하지만 함수는 호출할 때 실인수의 타입을 보고 구체화할 함수를 결정한다. 실인수가 생략되어 버리면 도대체 어떤 타입의 함수를 원하는지 컴파일러가 알 방법이 없기 때문이다.


31-2-마.특수화

클래스 템플릿도 함수 템플릿과 마찬가지로 실제 클래스 타입이 사용될 때만 구체화된다. 만약 특정 타입에 대해 미리 클래스 선언을 만들어 놓을 필요가 있다면 명시적 구체화를 할 수 있다. 예를 들어 float 타입의 PosValue 클래스를 미리 정의해 두고 싶다면 다음과 같이 한다.

template class PosValue<float>;

이 선언에 의해 컴파일러는 PosValue<float> 클래스를 미리 생성한다. 설사 이런 타입의 객체를 당장 선언하지 않는다 하더라도 컴파일러는 클래스 선언과 클래스 소속의 멤버 함수들을 모두 구체화해 둘 것이다. 특정 타입에 대한 클래스를 따로 생성하는 특수화도 물론 지원된다. 다음 예제는 tag_Friend 타입에 대해 PosValue 클래스를 특수화한다.

  : SpecializationClass
#include <Turboc.h>
#include <iostream>
using namespace std;

template <typename T>
class PosValue
{
private:
     int x,y;
     T value;
public:
     PosValue(int ax, int ay, T av) : x(ax),y(ay),value(av) { }
     void OutValue();
};

template <typename T>
void PosValue<T>::OutValue()
{
     gotoxy(x,y);
     cout << value << endl;
}

struct tag_Friend {
     char Name[10];
     int Age;
     double Height;
};

template <> class PosValue<tag_Friend>
{
private:
     int x,y;
     tag_Friend value;
public:
     PosValue(int ax, int ay, tag_Friend av) : x(ax),y(ay),value(av) { }
     void OutValue();
};

void PosValue<tag_Friend>::OutValue()
{
     gotoxy(x,y);
     cout << "이름:" << value.Name << ", 나이:" << value.Age
          << ", 키:" << value.Height << endl;
}

void main()
{
     PosValue<int> iv(1,1,2);
     tag_Friend F={"아무개",25,177.7};
     PosValue<tag_Friend> fv(2,2,F);
     iv.OutValue();
     fv.OutValue();
}

PosValue 클래스는 위치를 가지는 임의 타입의 값을 표현하는데 임의 타입이라고 했으므로 int, char, double 등의 표준 타입은 물론이고 구조체나 클래스 타입에 대해서도 동작해야 한다. 그러나 OutValue 멤버 함수가 값 출력을 위해 cout 표준 출력 객체를 사용하기 때문에 사실상 cout이 인식하는 타입에 대해서만 지원하는 셈이다. tag_Friend 구조체 타입에 대한 PosValue 클래스를 작성하려면 이 타입에 대한 특수화된 버전을 만들고 OutValue 함수의 코드를 조금 다르게 작성할 필요가 있다. 특수화를 할 때는 다음 형식으로 클래스를 정의한다.

template<> class 클래스명<특수타입>

이렇게 정의하면 지정한 타입에 대해 특수화된 클래스를 생성한다. 인수의 타입이 이미 결정되어 있으므로 특수화된 클래스의 멤버 함수를 외부에서 정의할 때는 template < >를 붙이지 않아도 상관없다. OutValue 함수는 tag_Friend 구조체의 각 멤버를 순서대로 출력하도록 수정했는데 원래의 PosValue 템플릿에 있는 OutValue와는 코드가 다르다. 실행해 보면 (2,2) 위치에 구조체 F의 내용이 출력될 것이다.
특수화를 하면 특수화된 클래스는 객체를 선언하지 않더라도 자동으로 구체화된다. 즉, 클래스 정의가 만들어지고 멤버 함수들은 컴파일되어 실행 파일에 포함된다. 따라서 특수화된 클래스에 대한 정의는 일반적인 템플릿 클래스와는 달리 헤더 파일에 작성해서는 안되며 구현 파일에 작성해야 한다. 예제에서는 구조체에 대해서도 PosValue 템플릿을 쓰기 위해 특수화를 사용했는데 사실 이보다 더 간단한 방법은 tag_Friend 구조체가 << 연산자를 오버로딩해서 기존 템플릿의 본체 코드를 지원하는 것이다.
부분 특수화(Partial Specialization)란 템플릿 인수가 여러 개 있을 때 그 중 하나에 대해서만 특수화를 하는 기법이다. 다음 템플릿을 보자.

template <typename T1, typename T2> class SomeClass { ... }

SomeClass 클래스 템플릿은 두 개의 인수를 가지므로 <int, int>, <int, double>, <short, unsigned> 등 두 타입의 조합을 마음대로 선택할 수 있다. 부분 특수화는 이 중 하나의 타입은 마음대로 선택하도록 그대로 두고 나머지 하나에 대해서만 타입을 강제로 지정하는 것이다. T2가 double인 경우에 대해서만 특수화를 하고 싶다면 다음과 같이 한다.

template <typename T1> class SomeClass<T1, double> { ... }

이 상태에서 SomeClass<int, unsigned>나 SomeClass<float, short>는 특수화되지 않은 버전의 템플릿으로부터 생성되지만 SomeClass<int, double>이나 SomeClass<char, double>은 부분 특수화된 템플릿으로부터 생성될 것이다. 두 번째 인수가 double인 클래스에 대해서만 부분적으로 특수화를 했기 때문이다. gcc는 부분 특수화를 지원하지만 비주얼 C++ 6.0에서는 지원되지 않는다.




31-3.컨테이너

31-3-가.TDArray

C++이 지원하는 템플릿 개념은 사실 그다지 어렵지 않다. 하지만 여기에 구체화, 특수화, 템플릿 중첩, 프렌드와의 관계, 정적 멤버 등이 개입되면 상당히 복잡한 문법이 만들어지며 표기법도 생소해서 쉽게 익숙해지기 어렵다. 또한 잘 만들어 놓았더라도 템플릿 클래스의 타입이 길고 복잡해서 코드의 의미를 얼른 파악하기도 무척 어렵다. 이런 복잡성에도 불구하고 C++이 템플릿을 지원하는 이유는 컨테이너를 만들기 위해서라고 해도 과언이 아니다.
컨테이너(Container)란 객체의 집합을 다룰 수 있는 객체이다. 쉽게 말해서 배열이나 연결 리스트같은 것들을 컨테이너라고 하는데 동일한 타입(또는 호환되는 타입)의 객체들을 저장하며 이런 객체들을 관리할 수 있는 기능을 가지는 또 다른 객체이다. 2부에서 우리는 동적으로 크기를 변경할 수 있는 동적 배열을 만들어 본 바 있으며 또한 앞 장에서 동적 배열의 기능을 캡슐화한 DArray라는 클래스도 작성해 보았다.
이 클래스가 무척 실용적이라는 것은 경험해 보아서 알 것이고 클래스로 캡슐화하면 사용하기도 무척 편리하다. 이렇게 만들어진 DArray가 바로 컨테이너이다. 그러나 아직 아쉬움이 있는데 바로 ELETYPE이라는 매크로로 배열 요소의 타입을 결정해야 한다는 점이다. DArray 클래스를 사용하기 전에 ELETYPE을 원하는 타입으로 바꿔야 하고 int 배열과 double 배열을 동시에 사용할 수도 없어서 활용성이 크게 떨어진다.
이런 문제를 해결하기 위해 만들어진 문법이 바로 템플릿이다. 요소의 타입은 객체를 선언하는 시점으로 연기해 두고 일단 필요한 알고리즘만 템플릿에 작성한다. 이후 타입만 바꾸면 이 타입을 요소로 가지는 동적 배열 클래스를 만드는 작업은 컴파일러가 알아서 할 것이다. 다음 소스는 DArray 클래스의 템플릿 버전인 TDArray이며 원칙에 따라 TDArray.h라는 헤더 파일에 작성했다.

  : TDArray.h
template <typename T>
class TDArray
{
protected:
     T *ar;
     unsigned size;
     unsigned num;
     unsigned growby;

public:
     TDArray(unsigned asize=100, unsigned agrowby=10);
     virtual ~TDArray();
     virtual void Insert(int idx, T value);
     virtual void Delete(int idx);
     virtual void Append(T value);

     T GetAt(int idx) { return ar[idx]; }
     unsigned GetSize() { return size; }
     unsigned GetNum() { return num; }
     void SetAt(int idx, T value) { ar[idx]=value; }
     void Dump(char *sMark);
};

template <typename T>
TDArray<T>::TDArray(unsigned asize, unsigned agrowby)
{
     size=asize;
     growby=agrowby;
     num=0;
     ar=(T *)malloc(size*sizeof(T));
}

template <typename T>
TDArray<T>::~TDArray()
{
     free(ar);
}

template <typename T>
void TDArray<T>::Insert(int idx, T value)
{
     unsigned need;

     need=num+1;
     if (need > size) {
          size=need+growby;
          ar=(T *)realloc(ar,size*sizeof(T));
     }
     memmove(ar+idx+1,ar+idx,(num-idx)*sizeof(T));
     ar[idx]=value;
     num++;
}

template <typename T>
void TDArray<T>::Delete(int idx)
{
     memmove(ar+idx,ar+idx+1,(num-idx-1)*sizeof(T));
     num--;
}

template <typename T>
void TDArray<T>::Append(T value)
{
     Insert(num,value);
}

template <typename T>
void TDArray<T>::Dump(char *sMark)
{
     unsigned i;
     cout << sMark << " => 크기=" << size << ",개수=" << num << " : ";
     for (i=0;i<num;i++) {
          cout << GetAt(i) << ' ';
     }
     cout << endl;
}

DArray에 비해 어떤 점이 바뀌었는지 보자. 일단 이름이 바뀌었는데 굳이 이름을 바꿀 필요는 없지만 템플릿 버전이라는 것을 분명히 표시하기 위해 앞에 T자를 하나 더 붙였다. ELETYPE 매크로는 사라졌으며 클래스 정의문앞에 template <typename T>가 추가되었고 소스내의 모든 ELETYPE은 T로 대체했다. 이제 배열 요소의 타입은 매크로가 아닌 템플릿 인수에 의해 결정되며 객체를 선언할 때마다 원하는 타입을 지정할 수 있다.
멤버 함수의 소속은 모두 TDArray<T> 클래스가 되며 함수 본체의 ELETYPE은 T로 바뀐다. 멤버 함수의 코드는 원칙적으로 변경할 필요가 없다. 기존 클래스가 템플릿화되면서 꼭 바뀌어야 하는 부분은 사실상 없는 셈이며 만약 있다면 이는 그 클래스가 템플릿화를 할만큼 충분히 일반화되지 못한 것이다.
이제 TDArray는 임의의 타입에 대한 동적 배열을 만들 수 있는 클래스 템플릿이 되었으며 이 안에는 동적 배열을 관리하는 모든 알고리즘이 포함되어 있다. 어디까지나 템플릿일 뿐이므로 아직 클래스는 아니지만 원하는 타입과 함께 객체를 선언하면 컴파일러에 의해 구체화된다. 제대로 동작하는지 예제를 작성해 보자.

  : TDArrayTest
#include <Turboc.h>
#include <iostream>
using namespace std;
#include "TDArray.h"

void main()
{
     TDArray<int> ari;
     TDArray<double> ard;
     int i;

     for (i=1;i<=5;i++) ari.Append(i);
     ari.Dump("5개 추가");
     for (i=1;i<=3;i++) ard.Append((double)i*1.23);
     ard.Dump("3개 추가");
}

TDArray.h 헤더 파일만 포함하면 템플릿이 정의된다. main에서는 정수형 동적 배열 TDArray<int> 타입의 객체 ari와 실수형 동적 배열 TDArray<double> 타입의 ard 객체를 선언했으며 잘 동작하는지 확인해 보기 위해 값을 추가한 후 Dump만 해 보았다.

5개 추가 => 크기:100,개수:5 값 : 1 2 3 4 5
3개 추가 => 크기:100,개수:3 값 : 1.23 2.46 3.69

정수에 대해서나 실수에 대해서나 TDArray는 잘 작동함을 확인할 수 있다. TDArray.h 헤더 파일만 포함시키고 객체를 선언할 때 원하는 타입만 밝히면 임의 타입에 대해 동작하는 동적 배열을 쉽게 사용할 수 있다. TDArray는 임의 타입에 대해서 잘 동작하는 배열이기는 하지만 모든 경우에 두루 쓸 수 있는 일반성을 갖추지는 못했다. 내부에서 동적 할당을 하므로 복사 생성자와 대입 연산자를 원칙대로 적절히 정의해야 한다.
또한 동적 할당되는 포인터에 대한 배열이나 클래스에 대한 배열로 쓰기에는 조금 불편한 점이 있다. 포인터의 경우 삭제할 때 포인터가 가리키는 곳도 해제하는 것이 좋을 것이고 객체의 경우 생성자와 파괴자도 호출해 주면 편리하다. 물론 그렇다고 해서 TDArray를 포인터나 객체의 배열로 쓸 수 없다는 얘기는 아니며 외부에서 관리해야 한다는 점이 불편할 뿐이다. TDArray는 어디까지나 예제일 뿐이고 훨씬 더 잘 만들어진 동적 배열 템플릿(예:vector, CTypedPtrArray) 들이 많이 공개되어 있으므로 실무를 할 때는 이런 것들을 쓰기 바란다.


31-3-나.TStack

다음 예제는 19장에서 작성했던 정수형 스택을 클래스로 만든 것이다. 스택을 표현하는데 필요한 정수형 배열과 배열 크기, 스택 포인터 등을 멤버 변수로 포함시키고 Push, Pop 등의 기본 동작은 멤버 함수로 구현했다. 스택을 초기화하는 기능은 생성자에 작성하고 해제하는 코드는 파괴자에 두면 된다.

  : iStack
#include <Turboc.h>

class iStack
{
private:
     int *Stack;
     int Size;
     int Top;

public:
     iStack(int aSize) {
          Size=aSize;
          Stack=(int *)malloc(Size*sizeof(int));
          Top=-1;
     }
     virtual ~iStack() {
          free(Stack);
     }
     virtual BOOL Push(int data) {
          if (Top < Size-1) {
              Top++;
              Stack[Top]=data;
              return TRUE;
          } else {
              return FALSE;
          }
     }
     virtual int Pop() {
          if (Top >= 0) {
              return Stack[Top--];
          } else {
              return -1;
          }
     }
};

void main()
{
     iStack iS(256);
     iS.Push(7);
     iS.Push(0);
     iS.Push(6);
     iS.Push(2);
     iS.Push(9);
     printf("%d\n",iS.Pop());
     printf("%d\n",iS.Pop());
     printf("%d\n",iS.Pop());
     printf("%d\n",iS.Pop());
     printf("%d\n",iS.Pop());
}

스택이 필요할 때는 iStack 클래스의 인스턴스를 생성하는데 인수로 스택의 크기만 밝히면 나머지 필요한 초기화는 생성자에서 한다. 그리고 Push, Pop 등의 멤버 함수를 호출하여 스택을 편리하게 사용할 수 있다. 다 사용한 스택은 파괴자에서 정리하므로 별도의 해제를 할 필요가 없다.
동작에 필요한 필수 멤버들이 한 클래스에 캡슐화되어 있으므로 확실히 사용하기에는 편리하다. 그러나 아직 타입에 대한 종속성을 해결하지는 못했는데 iStack은 정수형 값만 저장할 수 있을 뿐이며 double이나 char 형을 저장할 수는 없다. 다른 타입에 대한 스택을 만들려면 iStack의 일부를 수정해야 하는데 Stack 멤버의 타입, Push의 인수, Pop의 리턴 타입 정도만 바꾸면 된다. 타입만 변경될 뿐 알고리즘은 동일하므로 템플릿을 사용하면 임의 타입을 지원할 수 있다.
19장에서 실수형 스택과 문자형 스택을 사용하여 수식을 계산하는 TextCalc 예제를 만들어 본 적이 있는데 잘 동작하기는 하지만 똑같은 논리를 사용하는 스택이 두 카피 존재한다는 점이 무척 불합리해 보인다. 단지 타입만 다를 뿐인데 이것들을 하나로 합칠 수가 없는 것이다. 이렇게 되면 코드의 양이 많아지는 것은 물론이고 기능을 확장할 때도 일일이 두 군데를 고쳐야 하므로 유지하기가 아주 어려워진다. 템플릿을 사용하여 스택 하나로 두 가지 타입을 지원하도록 수정해 보자. 먼저 스택 클래스 템플릿을 헤더 파일에 작성한다.

  : TStack.h
template<typename T>
class TStack
{
protected:
     T *Stack;
     int Size;
     int Top;

public:
     TStack(int aSize) {
          Size=aSize;
          Stack=(T *)malloc(Size*sizeof(T));
          Top=-1;
     }
     virtual ~TStack() {
          free(Stack);
     }
     virtual BOOL Push(T data) {
          if (Top < Size-1) {
              Top++;
              Stack[Top]=data;
              return TRUE;
          } else {
              return FALSE;
          }
     }
     virtual T Pop() {
          return Stack[Top--];
     }
     virtual int GetTop() { return Top; }
     virtual T GetValue(int n) { return Stack[n]; }
};

템플릿 기반으로 수정했으므로 이름을 TStack이라고 지었으며 스택에 저장할 타입을 인수열로 전달받는다. 멤버 함수의 논리는 앞의 iStack 예제와 동일하며 int가 들어가야 할 위치에 T가 대신 들어갔을 뿐이다. 몇 가지 차이점도 있는데 우선 에러 처리를 위해 Top 위치를 조사하는 GetTop 멤버 함수와 우선 순위 조사를 위해 지정한 위치의 값을 삭제하지는 않고 읽기만 하는 GetValue 함수가 추가되었다.
그리고 Pop 함수의 에러 처리 코드가 삭제되었는데 임의의 타입에 대해 동작하기 위해서는 -1을 리턴하는 단순한 방법을 쓸 수 없기 때문이다. -1이라는 특이값은 수치형에만 존재하므로 스택에 객체를 저장할 때는 에러를 처리하는 다른 방법이 필요하다. 스택에 아무 것도 없는 상태에서 pop을 호출하는 것은 분명한 논리적 에러이므로 assert를 쓰는 것이 가장 합리적이다. 이 템플릿이 제대로 동작하는지 계산기 예제를 만들어 보자.

  : TextCalcTemplate
#include <Turboc.h>
#include <math.h>
#include "TStack.h"

int GetPriority(int op)
{
     switch (op) {
     case '(':
          return 0;
     case '+':
     case '-':
          return 1;
     case '*':
     case '/':
          return 2;
     case '^':
          return 3;
     }
     return 100;
}

void MakePostfix(char *Post, const char *Mid)
{
     const char *m=Mid;
     char *p=Post,c;
     TStack<char> cS(256);

     while (*m) {
          // 숫자 - 그대로 출력하고 뒤에 공백 하나를 출력한다.
          if (isdigit(*m)) {
              while (isdigit(*m) || *m=='.') *p++=*m++;
              *p++=' ';
          } else
          // 연산자 - 스택에 있는 자기보다 높은 연산자를 모두 꺼내 출력하고 자신은 푸시한다.
          if (strchr("^*/+-",*m)) {
              while (cS.GetTop()!=-1 && GetPriority(cS.GetValue(cS.GetTop())) >=
                   GetPriority(*m)) {
                   *p++=cS.Pop();
              }
              cS.Push(*m++);
          } else
          // 여는 괄호 - 푸시한다.
          if (*m=='(') {
              cS.Push(*m++);
          } else
          // 닫는 괄호 - 여는 괄호가 나올 때까지 팝해서 출력하고 여는 괄호는 버린다.
          if (*m==')') {
              for (;;) {
                   c=cS.Pop();
                   if (c=='(') break;
                   *p++=c;
              }
              m++;
          } else {
              m++;
          }
     }
     // 스택에 남은 연산자들 모두 꺼낸다.
     while (cS.GetTop() != -1) {
          *p++=cS.Pop();
     }
     *p=0;
}

double CalcPostfix(const char *Post)
{
     const char *p=Post;
     double num;
     double left,right;
     TStack<double> dS(256);

     while (*p) {
          // 숫자는 스택에 넣는다.
          if (isdigit(*p)) {
              num=atof(p);
              dS.Push(num);
              for(;isdigit(*p) || *p=='.';p++) {;}
          } else {
              // 연산자는 스택에서 두 수를 꺼내 연산하고 다시 푸시한다.
              if (strchr("^*/+-",*p)) {
                   right=dS.Pop();
                   left=dS.Pop();
                   switch (*p) {
                   case '+':
                        dS.Push(left+right);
                        break;
                   case '-':
                        dS.Push(left-right);
                        break;
                   case '*':
                        dS.Push(left*right);
                        break;
                   case '/':
                        if (right == 0.0) {
                             dS.Push(0.0);
                        } else {
                             dS.Push(left/right);
                        }
                        break;
                   case '^':
                        dS.Push(pow(left,right));
                        break;
                   }
              }
              // 연산 후 또는 연산자가 아닌 경우 다음 문자로
              p++;
          }
     }
     if (dS.GetTop() != -1) {
          num=dS.Pop();
     } else {
          num=0.0;
     }
     return num;
}

double CalcExp(const char *exp,BOOL *bError=NULL)
{
     char Post[256];
     const char *p;
     int count;
    
     if (bError!=NULL) {
          for (p=exp,count=0;*p;p++) {
              if (*p=='(') count++;
              if (*p==')') count--;
          }
          *bError=(count != 0);
     }

     MakePostfix(Post,exp);
     return CalcPostfix(Post);
}

void main()
{
     char exp[256];
     BOOL bError;
     double result;

     char *p=strchr("^*/+-",NULL);
     strcpy(exp,"2.2+3.5*4.1");printf("%s = %.2f\n",exp,CalcExp(exp));
     strcpy(exp,"(34+93)*2-(43/2)");printf("%s = %.2f\n",exp,CalcExp(exp));
     strcpy(exp,"1+(2+3)/4*5+2^10+(6/7)*8");printf("%s = %.2f\n",exp,CalcExp(exp));

     for (;;) {
          printf("수식을 입력하세요(끝낼 때 0) : ");
          gets(exp);
          if (strcmp(exp,"0")==0) break;
          result=CalcExp(exp,&bError);
          if (bError) {
              puts("수식의 괄호짝이 틀립니다.");
          } else {
              printf("%s = %.2f\n",exp,result);
          }
     }
}

실행해 보면 잘 계산된다.

2.2+3.5*4.1 = 16.55
(34+93)*2-(43/2) = 232.50
1+(2+3)/4*5+2^10+(6/7)*8 = 1038.11
수식을 입력하세요(끝낼 때 0) : (1+2+3)*4
(1+2+3)*4 = 24.00

중위식을 후위식으로 변환하는 MakePostfix 함수는 문자형의 스택이 필요하므로 TStack<char> 타입의 cS를 256크기로 선언해서 사용한다. 이 선언문에 의해 컴파일러는 char 타입에 대한 스택 클래스를 구체화할 것이다. cS가 지역변수이므로 함수가 종료될 때 자동으로 파괴되며 따라서 별도의 정리 코드를 작성할 필요가 없다.
후위식을 연산하는 CalcPostfix 함수는 연산 과정의 중간값 저장을 위해 실수형 스택이 필요하다. 그래서 TStack<double> 타입의 dS를 선언해서 사용하고 있다. 두 함수가 각각 타입이 다른 스택을 만들어 사용하지만 클래스가 템플릿으로 선언되어 있으므로 아무 문제가 없다. 필요하다면 얼마든지 많은 타입에 대해 스택을 만들어 쓸 수 있을 것이다.


31-3-다.템플릿 중첩

템플릿의 인수열에 들어갈 수 있는 타입에는 특별한 제한이 없다. 기본 타입은 물론이고 클래스 타입도 템플릿의 인수열에 넣을 수 있다. 그렇다면 템플릿으로 만든 클래스도 분명히 타입의 일종이므로 다른 템플릿의 인수가 될 수 있다는 얘기인데 즉, 템플릿끼리 중첩될 수 있다. 다음 예제는 PosValue 템플릿 클래스를 요소로 가지는 스택을 정의한다.

  : NestTemplate
#include <Turboc.h>
#include <iostream>
using namespace std;
#include "TStack.h"

template <typename T>
class PosValue
{
private:
     int x,y;
     T value;
public:
     PosValue() : x(0),y(0),value(0) { }
     PosValue(int ax, int ay, T av) : x(ax),y(ay),value(av) { }
     void OutValue() {
          gotoxy(x,y);
          cout << value << endl;
     }
};

void main()
{
     TStack<PosValue<int> > sPos(10);

     PosValue<int> p1(5,5,123);
     PosValue<int> p2;
     sPos.Push(p1);
     p2=sPos.Pop();
     p2.OutValue();
}

선두에는 TStack 클래스 템플릿과 PosValue 클래스 템플릿이 선언되어 있으며 이 두 템플릿으로 임의의 타입에 대한 스택과 PosValue 객체를 만들 수 있다. 초기화되지 않은 객체를 만들 수 있도록 하기 위해 PosValue에 디폴트 생성자를 추가로 정의했다. main에서는 다소 복잡한 형식을 가지는 sPos라는 객체를 선언하고 있는데 이 객체는 TStack으로부터 만들어졌으므로 일단은 스택이다. 스택에 들어가는 요소는 인수열에 있는 PosValue<int> 타입이므로 이런 객체들의 임시 저장소가 된다.
main의 나머지 코드는 PosValue<int>형의 객체 p1, p2 둘을 선언하고 p1을 스택에 푸시한 후 p2로 팝해 보았다. 푸시한 값을 그대로 빼내 대입했으므로 p2가 p1과 같아질 것이다. p2의 값을 출력해 보면 p1의 생성자에서 초기화한 위치에 123이라는 값이 출력될 것이다. 컴파일러는 중첩된 선언문에서 안쪽 클래스부터 차례대로 구체화한다. 템플릿끼리 중첩되어 있을 뿐이지 별다른 사항은 없다. 단, 이런 중첩 템플릿 선언문을 작성할 때 다음과 같이 작성해서는 안된다.

TStack<PosValue<int>> sPos(10);

템플릿 인수열안에 인수열이 있으므로 닫는 괄호 >가 두 번 연거푸 나오는데 이렇게 되면 컴파일러는 >>를 오른쪽 쉬프트 연산자로 해석하게 된다. 선언문에 연산자가 올 수 없으므로 이 문장은 에러로 처리될 것이다. 그래서 템플릿끼리 중첩될 때 인수열의 닫는 괄호 사이에는 반드시 공백을 하나 넣어 쉬프트 연산자와 구분되도록 해야 한다.
C++은 템플릿의 중첩을 문법적으로 허가하므로 이중 삼중으로 템플릿을 중첩할 수도 있다. 그러나 문법과는 별개로 템플릿끼리 중첩되려면 두 클래스가 임의의 타입에 대해서도 잘 동작할 수 있도록 충분히 일반화되어 있어야 한다. 대상 타입을 수치형으로 가정하여 -1같은 특이값을 사용해서는 안되며 대입 연산을 하는 요소는 대입 연산자를 적절하게 오버로딩해야 한다. 출력문으로 cout을 사용한다면 대상 타입은 << 연산자도 정의해야 한다.
템플릿끼리 중첩 가능하므로 자신이 자신을 포함하는 템플릿을 만들 수 있다. TDArray 클래스 템플릿은 임의의 타입을 배열 요소로 가질 수 있는데 그 타입을 TDArray로 준다면 동적 배열의 동적 배열을 만드는 것도 가능하다는 얘기이다. TDArray<TDArray<int> > ara;는 정수형을 요소로 가지는 동적 배열을 요소로 가지는 동적 배열 ara를 선언한 것이다.





댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...