책정리/혼자 연구하는 C,C++ 2

31장 템플릿

GONII 2015. 3. 5. 16:19

31.1 함수 템플릿

31.1.1 타입만 다른 함수들

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

  1. 구조적 프로그래밍

    C언어와 마찬가지로 함수 위주로 프로그램을 작성할 수 있다.

  2. 객체지향 프로그래밍

    캡슐화, 추상화를 통해 현실 세계의 사물을 모델링할 수 있으며 상속과 다형성을 지원하기 위한 여러가지 언어적 장치를 제공한다.

  3. 일반화 프로그래밍

    임의 타입에 대해 동작하는 함수나 클래스를 작성할 수 있다. 객체 지향보다 재사용성과 편의성이 더 우수하다.

일반화 프로그래밍은 주로 C++ 템플릿에 의해 지원되며 C++ 표준 라이브러리가 일반화의 좋은 예이다.

템플릿은 C++이 일반화를 위해 제공하는 가장 기본적인 문법이므로 템플릿에 대한 이해는 C++ 표준 라이브러리인 STL을 이해하기 위한 문법적 토대가 됨

템플릿(Template)이란 무엇인가를 만들기 위한 형틀이라는 뜻이다.

템플릿은 모양에 대한 본을 떠 놓은 것이며 한 번만 잘 만들어 놓으면 이후부터 재료만 집어넣어서 똑같은 모양을 손쉽게 여러 번 찍어낼 수 있다.

템플릿의 또 다른 특징은 집어넣은 재료에 따라 결과물들이 조금씩 달라진다는 것이다.

  • 예제 SwapFunc

#include <iostream>

using namespace std ;

   

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 ( void )

{

int a = 3, b = 4 ;

double c = 1.2, d = 3.4 ;

   

swap(a, b) ;

swap(c, d) ;

   

cout << "a = " << a << " b = " << b << endl ;

cout << "c = " << c << " d = " << d << endl ;

}

C++은 오버로딩을 지원하므로 함수의 이름이 똑같아도 인수만 다르게 하여 함수를 두개 만들었지만 C에서는 함수의 이름마저도 달라야 한다.

이런 비슷한 함수들을 일일이 만들어야 한다는 것은 무척 짜증나는 일이며 만든 후에 수정하기도 번거롭다. 그래서 이 함수들을 통합할 수 있는 여러 가지 방법들을 생각해 볼 수 있다.

   

  1. 우선 인수의 타입을 #define이나 typedef로 정의한 후 본체에서는 이 매크로를 참조하는 방법을 생각할 수 있다.

    #define SWAPTYPE int

    void Swap( SWAPTYPE &a, SWPATYPE &B ) ;

  2. 매크로 함수 사용

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

  3. 이외에 void*라는 일반적인 포인터 타입을 쓰는 방법도 있다.
  • 예제 SwapVoid

#include <iostream>

using namespace std ;

   

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 ( void )

{

int a = 3, b = 4 ;

double c = 1.2, d = 3.4 ;

Swap(&a, &b, sizeof(int)) ;

Swap(&c, &d, sizeof(double)) ;

cout << "a = " << a << ", b = " << b << endl ;

cout << "c = " << c << ", d = " << d << endl ;

}

void *를 이용한 교환 함수는 나름대로 실용성도 있고 그야말로 임의 타입을 다룰 수 있다는 점에서 훌륭하다. 실제로 이런 함수는 종종 사용되며 템플릿보다 더 우월한 면도 있다. 하지만 일일이 &를 붙여 번지를 전달해야 하고 길이까지 가르쳐 주어야 한다는 점에서 불편하기는 마찬가지이다.

원하는 함수의 모양을 템플릿으로 등록해 두면 함수를 만드는 나머지 작업은 컴파일러가 알아서 한다.

  • 예제 SwapTemp

#include <iostream>

using namespace std ;

   

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 ( void )

{

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 } ;

   

cout << "before a = " << a << " b = " << b << endl ;

Swap(a, b) ;

cout << "after a = " << a << " b = " << b << endl ;

Swap(c, d) ;

Swap(e, f) ;

Swap(g, h) ;

}

Swap 함수 템플릿을 정의한 후 정수, 실수, 문자열, 구조체 등에 대해 Swap 함수를 호출해 보았다.

임의 타입에 대해 Swap 함수를 사용할 수 있되 단 함수 내에서 지역적으로 선언된 타입은 사용할 수 없다.

함수 호출부에서 int 타입을 사용했으면 T는 int가 되며 함수 본체에서 참조하는 T는 모두 int가 될 것이다.

템플릿이란 컴파일러가 미리 등록된 함수의 형틀을 기억해 두었다가 함수가 호출될 때 실제 함수를 만드는 장치이다.

매크로함수와의 다른점은 매크로 함수는 전처리기가 처리하지만 템플릿은 컴파일러가 직접 처리한다. 전처리기는 지시대로 소르를 재구성할 뿐이므로 개발자가 필요한 타입에 대해 일일이 매크로를 전개해야 하므로 수동이지만 템플릿은 호출만 하면 컴파일러가 알아서 함수를 만드는 자동식이다.

   

템플릿 인수 목록에서 두 개 이상의 타입을 전달받을 수도 있다.

template <typename T1, typename T2>

템플릿 정의는 함수 호출부보다 먼저 와야 한다.

template <typename T>

void Swap( T &a, T &b);

31.1.2 구체화

함수 템플릿으로부터 함수를 만드는 과정을 구체화 또는 인스턴스화(Instantiation)라고 하는데 호출에 의해 구체화되어야만 실제 함수가 만들어진다.

만약 템플릿만 정의하고 함수를 호출하지 않으면 아무런 일도 일어나지 않으며 템플릿 자체는 메모리를 소모하지 않는다. 호출에 의해 템플릿이 구체화되어 실제 함수가 될 때만 프로그램의 크기가 늘어난다.

컴파일러에 의해 구체화된 함수는 실행 파일에 실제로 존재하며 컴파일 단계에서 이미 만들어지므로 실행시의 부담은 전혀 없다. 함수가 호출될 때 만들어지는 것이 아니다. 대신 매 타입마다 함수들이 새로 만들어지므로 구체화되는 수만큼 실행 파일의 용량이 늘어난다. 템플릿은 크기를 포기하는 대신 속도를 얻는 방식인데 크기와 속도는 항상 반비례 관계에 있다.

   

:: 명시적 인수

컴파일러는 호출부의 실인수 타입을 판별하여 필요한 함수를 구체화하는데 예를 들어 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)호출에 대해 a를 double로 암시적 변환해서 호출할 수도 있을 것 같지만 템플릿은 타입이 정확해야 하므로 암시적 변환까지는 고려하지 않는다.

상수는 변수와 달리 그 형태만으로 타입을 정확하게 판단하기 힘든 경우가 있다. 그래서 템플릿 함수를 호출 할 때 실인수와는 다른 타입을 강제로 지정할 수 있는데 이 때는 함수명 다음의 < > 괄호 안에 원하는 타입을 밝힌다.

리턴 타입이나 인수로 직접 사용되지 않는 타입을 가지는 함수를 호출하기 위해서는 명시적으로 템플릿의 인수 타입을 지정해야 한다. 리턴 타입은 호출할 함수를 결정할 때는 사용되지 않으며 또한 인수로 전달되지 않고 함수 내부에서만 사용하는 타입도 함수 호출문에는 나타나지 않는다. 이럴 때는 컴파일러가 함수 호출문만으로 구체화할 함수를 결정할 수 없으므로 어떤 타입의 템플릿 함수를 원하는지 분명히 지정해야 한다.

  • 예제 TempReturn

#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);

   

cout << "i = " << i << ", d = " << d << endl ;

func<int>();

}

cast는 인수로 전달된 s를 템플릿 인수가 지정하는 타입으로 캐스팅하는 함수이다. cast(1234) 호출문만으로는 어떤 버전의 함수를 만들지 결정할 수 없으므로 명시적으로 인수를 밝혀서 호출해야 한다. 이처럼 리턴 타입만 다른 경우라면 템플릿에 의해 각각 따로 구체화될 수는 있지만 호출할 때 어떤 함수를 호출하는지를 반드시 밝혀야 한다.

func 함수는 내부적인 처리를 위해 T형의 지역변수 v를 선언하여 사용한다. 물론 T가 가변적인 타입이므로 본체는 전달된 모든 타입에 대해 가능한 코드만 사용해야 한다. func는 인수도 리턴값도 없으므로 호출부만 봐서는 어떤 함수를 구체화할지 전혀 결정할 수 없다. 따라서 func()라고 호출만하면 컴파일러가 에러를 발생한다. 이 때도 func<int>()처럼 지역변수 v의 타입을 명시적으로 전달해야 한다.

리턴타입만 다른 템플릿이나 알지도 못하는 타입의 지역변수를 선언하는 함수는 그다지 실용성이 없어 보이지만 호환되는 여러 가지 타입의 객체 중 원하는 것을 선택해서 대신 생성해주는 래퍼 함수를 만들고 싶을 때 가끔 사용되기도 한다.

  • 예제 ExplicitPara

#include <iostream>

   

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);

   

:: 명시적 구체화

함수의 호출부를 보고 컴파일러가 템플릿 함수를 알아서 만드는 것을 암시적 구체화라고 한다. 개발자가 원하는 타입으로 함수를 호출하기만 하면 나머지는 컴파일러가 다 알아서 하며 호출하지 않는 타입에 대해서는 구체화하지 않는다.

만약 특정 타입에 대한 템플릿 함수를 강제로 만들고 싶다면 이 때는 명시적 구체화(Explicit Instantiation)를 하는데 이는 지정한 타입에 대해 함수를 생성하도록 컴파일러에게 지시하는 것이다.

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

명시적 구체화 명령의 표기는 일단 키워드 template가 앞에 오고 함수 이름 다음에 생성하고 싶은 타입을 < > 괄호 안에 적는다. 이 선언에 의해 float형을 인수로 취하는 Swap(float, float) 함수가 만들어진다. 템플릿이 어떤 모양인지 알아야 컴파일러가 이런 함수를 만들 수 있으므로 명시적 구체화 명령은 템플릿 선언보다 뒤에 와야 한다.

이 함수가 당장 필요치 않더라도 일단 만들어 놓고 싶다면 명시적 구체화로 강제 생성을 지시할 수 있다. 함수의 내용을 숨기고 싶을 때는 함수 템플릿을 공개할 수 없으므로 이럴 때는 명시적 구체화로 자주 사용할만한 타입에 대해 일련의 합수 집합을 미리 생성해 놓는다. 이 라이브러리의 사용자는 개발자가 명시적으로 구체화해 놓은 함수만 사용할 수 있을 것이다. 명시적 구체화는 컴파일 속도에도 긍정적이 효과가 있는데 미리 필요한 함수를 생성해 놓으면 컴파일러가 어떤 함수를 생성할 것인지를 판단하는 시간을 조금 절약할 수 있다.

31.1.3 동일한 알고리즘 조건

함수 템플릿은 코드는 동일하고 타입만 다른 함수의 집합을 정의한다.

  • 예제 TemplateFunc

#include <iostream>

   

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 함수들이 템플릿으로 정의되어 있다. 이 함수들은 인수로 전달된 임의의 타입에 대해 동작할 수 있으며 호출부에서는 실인수의 타입을 보고 적절한 함술르 구체화하여 호출한다.

만약 알고리즘이 동일하지 않다면, 즉 함수의 본체가 완전히 달라야 한다면 이 함수들은 같은 템플릿으로 통합될 수 없다.

  • 예제 SwapArray

#include <iostream>

   

template <typename 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);

}

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

31.1.4 임의 타입 지원 조건

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

template < typename T >

void printValue(T value)

{

printf("value is %d\n", value);

}

템플릿으로 되어 있어서 임의의 타입을 받을 수는 있지만 출력 코드에서 %d 서식을 사용하고 있으므로 사실상 정수 호환 타입만 출력할 수 있다.

   

앞 예제의 Add함수 템플릿은 + 연산자로 피연산자를 더하는데 +는 대부분의 기본 타입에 대해 오버로딩되어 있으므로 아무 타입이나 잘 더할 수 있을 것같다. 그러나 이 연산자를 쓸 수 있는 정수, 실수, 문자형 등의 수치형에 대해서만 사용할 수 있을 뿐이다. 문자열(char*)을 더할 때는 포인터끼리 더할 수 없으므로 strcat 함수를 사용해야 한다. 더하는 알고리즘이 완전히 다르므로 Add함수를 사용할 수 없다. +연산자를 오버로딩하고 있는 문자열 객체라면 이 함수로 연결할 수 있을 것이다.

  • 예제 MaxObject

#include <iostream>

using namespace std;

   

template<typename T>

T Max( T a, T b )

{

return ( a > b ) ? a : b ;

}

   

struct S

{

int i;

S(int ai) : i(ai) {}

//int 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) 호출도 잘 컴파일된다.

  • 예제 SwapPerson

#include <iostream>

   

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("aaa", 20);

A.outPerson();

B.outPerson();

   

Swap(A,B);

A.outPerson();

B.outPerson();

   

}

예제 선두에는 Swap 템플릿이 정의되어 있다. 변수 교환 알고리즘이 워낙 간단해서 별 문제가 없을 것 같지만 person 객체에 대해서는 제대로 동작하지 않으며 다운된다.

person 클래스는 대입 연산자를 제대로 정의하지 않아 템플릿의 코드와는 맞지 않다.

컴파일러는 함수 호출부의 타입을 보고 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.5 특수화

같은 템플릿으로 만들어진 함수는 타입만 제외하고 동일한 본체를 가지므로 동작도 동일하다. 만약 특정 타입에 대해서만 다르게 동작하도록 하고 싶다면 이 때는 특수화(Specialization)라는 기법을 사용한다.

  • 예제 Specialization

#include <iostream>

   

template<typename 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 함수의 본체는 정수부만 교환하는 고유한 코드를 가진다.

   

컴파일러는 템플릿 함수 호출 구분이 있을 때 항상 템플릿의 정의보다 특수화된 정의에 우선권을 주므로 동일한 이름의 템플릿과 특수화 함수가 존재하면 특수화된 함수가 호출된다.

특수화 함수를 표기하는 방법

  1. template<> void Swap<double>(double &a, double &b)
  2. template<> void Swap<> (double &a, double &b)
  3. template<> void Swap(double &a, double &b)
  4. void Swap<double>(double &a, double &b)
  5. void Swap<>(double &a, double &b)
  6. void Swap(double &a, double &b)

31.2 클래스 템플릿

31.2.1 타입만 다른 클래스들

클래스 템플릿은 함수 템플릿과 비슷하되 찍어내는 대상이 클래스라는 것만 다르다. 구조나 구현 알고리즘은 동일하되 멤버의 타입만 다를 경우 클래스를 일일이 따로 만드는 대신 템플릿을 정의한 후 템플릿으로부터 클래스를 만들 수 있다.

  • 예제 PosValueTemp

#include <iostream>

#include <windows.h>

using namespace std;

   

void gotoxy(int x, int y);

   

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,2,3);

PosValue<char> cv(5,1,'c');

PosValue<double> dv(30, 2, 3.14);

   

iv.outValue();

cv.outValue();

dv.outValue();

}

   

void gotoxy(int x, int y)

{

COORD Pos={x,y};

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),Pos);

}

클래스 선언문 앞에 template<typename T>를 붙이고 타입에 종속적인 부분에만 T를 사용하면 된다. 클래스 템플릿으로부터 만들어지는 클래스를 템플릿 클래스라고 하는데 템플릿 클래스의 타입 명에는 < > 괄호가 항상 따라 다닌다. value가 int형인 클래스의 이름은 PosValue<int>, char형이면 PosValue<char> 이다.

단 예외적으로 생성자의 이름은 클래스의 이름을 따라가지만 클래스 템플릿의 경우 템플릿 이름을 사용해도 상관없다.

클래스 템필릿의 멤버 함수를 선언문 외부에서 작성할 때는 템플릿에 속한 멤버 함수임을 밝히기 위해 소속 클래스의 이름에도 <T>를 붙여야 하며 T가 템플릿 인수임을 명시하기 위해 template<typename T>가 먼저 와야 한다. outValue 멤버 함수는 PosValue<T> 클래스의 소속이며, 이때 T는 템플릿 인수 목록으로 전달된 타입의 이름이다.

tempate<typename T>

void PosValue<T>::outValue()

{

...

}

클래스 내부에서 인라인으로 함수를 선언할 때는 클래스 선언문 앞에 T에 대한 설명이 있으므로 이렇게 하지 않아도 상관없다.

   

템플릿 클래스로부터 객체를 선언할 때는 템플릿 이름 다음에 < > 괄호를 쓰고 괄호 안에 T로 전달될 타입의 이름을 명시해야 한다. PosValue<int>는 int 타입의 value를 멤버로 가지는 PosValue 템플릿 클래스를 의미하며 PosValue<double> 클래스의 value는 double 타입이 된다. 템플릿 클래스의 이름에는 타입이 분명히 명시 되어야 한다.

컴파일러는 객체 선언문에 있는 초기값의 타입으로부터 어떤 타입에 대한 클래스를 원하는지 알 수 있을 것도 같다. 그러나 생성자가 오버로딩 되어 있을 경우 이 정보만으로는 원하는 타입을 정확하게 판단하기 어렵다. 또한 생성자를 호출하기 전에 객체를 위한 메모리를 할당해야 하는데 이 시점에서 생성할 객체의 크기를 먼저 계산할 수 있어야 하므로 클래스 이름에 타입이 명시되어야 한다.

   

템플릿으로부터 만들어지는 클래스도 분명히 클래스이며 일반적인 클래스와 전혀 다를바가 없다. 템플릿 클래스로부터 상속하는 것도 가능하며 문법도 동일하되 기반 클래스의 이름에 < > 괄호가 사용된다.

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

템플릿 클래스가 다른 클래스의 기반 클래스로 사용되면 컴파일러는 클래스를 즉기 구체화한다. 이 클래스의 인스턴스 선언문이 없더라도 말이다.

   

템플릿으로부터 만들어지지 않은 일반 클래스의 특정 멤버 함수만 템플릿으로 선언하는 것도 가능하다. 멤버 함수도 분명히 함수이므로 타입에 따라 여러 벌이 필요하다면 원하는 함수 하나만 함수 템플릿으로 만들면 된다.

  • 예제 TempMember

#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");

}

31.2.2 템플릿의 위치

클래스 템플릿 선언문은 반드시 사용하기 전에 와야 한다. PosValueTemp 예제에서 보다시피 main 함수보다 템플릿 선언이 더 앞에 있는데 이 순서가 바뀌면 main에서 PosValue<int>가 무엇을 의미하는지 모르므로 에러로 처리된다. 단 템플릿 클래스의 멤버 함수 본체 정의문은 앞쪽에 이미 소속과 원형이 선언되어 있으므로 main 함수보다 뒤에 있어도 상관없다.

   

헤더파일 구현파일 분리

템플릿은 그냥 .h파일 안에 같이 넣기로 하자.

분리 하려면 export 키워드를 넣어야 하지만 실제로 지원하는 컴파일러도 있고 지원하지 않는 컴파일러도 있기 때문에 분리하기가 쉽지 않다.

그래서 템플릿 라이브러리들은 거의 대부분 소스가 공개되어 있다고 한다.

31.2.3 비타입 인수

템플릿의 인수 목록에 전달되는 것은 통상 타입이다. 알고리즘은 같되 타입만 다른 함수나 클래스를 작성하고 싶을 때 템플릿을 사용한다. 그러나 타입이 아닌 상수를 템플릿 인수로 전달할 수 있는데 이를 비타입 인수(Nontype Argument)라고 한다.

  • 예제 NonTypeArgument

#include <iostream>

   

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 함수가 전달된 첨자의 범위를 점검하므로 실수로 범위 바깥을 액세스해도 치명적인 에러를 발생시키지 않는다. 이외에 확장하기에 따라서 얼마든지 다양한 기능을 더 넣을 수 있을 것이다.

   

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>타입이기 때문이다.

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

int size = 5;

Array<int, size> ari; // 에러

size는 변수이며 이 값은 실행 중에 수시로 변할 수 있으므로 템플릿의 인수로 사용할 수 없다. 템플릿이란 컴파일러가 인수를 적용하여 컴파일 중에 클래스를 만들어 내는 형틀이므로 모든 정보를 컴파일 중에 알 수 있어야 한다. 실행 중에 없던 클래스를 만들어내는 기능이 아니라 컴파일 중에 구체화해야 하므로 변수는 쓸 수 없다. const int size = 5;로 상수 선언했다면 가능하다.

함수로도 비타입 인수를 전달할 수 있다. 단, 함수의 형식 인수 목록에 어떤 상수가 올 수는 없으므로 비타입 인수는 함수의 본체에만 사용해야 하며 함수 호출문에 템플릿 인수를 명시적으로 지정해야 한다.

  • 예제 NonTypeArgFunc

#include <iostream>

   

template<int N>

void func(void)

{

int ar[N];

   

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

}

   

void main()

{

func<5>();

func<6>();

}

비타입 인수는 함수의 인수와는 용도가 다른데 함수의 형식 인수는 실행 시간에 전달되는 변수이므로 배열 선언문 등 상수가 필요한 곳에 사용할 수 없지만 비타입 인수는 구체화될 때 함수 본체에 직접 기입되므로 상수 일 수 있다.

31.2.4 디폴트 템플릿 인수

함수의 디폴트 인수는 함수를 호출할 때 생략된 인수에 대해 기본적으로 적용되는 값이다. 클래스 템플릿에도 이와 비슷한 개념인 디폴트 템플릿 인수가 있는데 객체 선언문에서 인수를 생략할 경우 템플릿 선언문에서 지정한 디폴트가 적용된다.

template <typename T=int>

class PosValue

{

...

}

< > 괄호 안의 타입 이름 다음에 = 구분자를 쓰고 디폴트로 적용될 타입을 지정한다. 이제 별다른 지정이 없다면 T는 디폴트 타입인 int가 된다.

PosValue<double>과 같이 타입을 분명히 밝히면 디폴트는 무시된다.

타입을 여러 개 가지는 클래스의 경우 오른쪽 인수부터 차례대로 디폴트를 지정할 수 있으며 객체를 선언할 때는 오른쪽부터 순서대로 샹락 가능하다. 이 점도 함수의 디폴트 인수와 같다.

클래스 템플릿에는 디폴트 인수를 줄 수 있지만 함수 템플릿에는 디폴트를 정의할 수 없다. 클래스는 객체를 선언할 때 클래스 타입을 지정하므로 생략 가능하지만 함수는 호출 할 때 실인수의 타입을 보고 구체화할 함수를 결정한다. 실인수가 생략되어 버리면 어떤 타입의 함술르 원하는지 컴파일러가 알 방법이 없기 때문이다.

31.2.5 특수화

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

template class PosValue<flaot>;

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

  • 예제 SpecializationClass

#include <iostream>

#include <windows.h>

using namespace std;

   

void gotoxy(int x, int y);

   

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,3);

tag_friend f = {"abc", 25, 170.2};

PosValue<tag_friend> fv(2, 2, f);

iv.outValue();

fv.outValue();

}

   

void gotoxy(int x, int y)

{

COORD Pos={x,y};

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),Pos);

}

PosValue클래스는 위치를 가지는 임의 타입의 값을 표현하는데 임의 타입이라고 했으므로 int, char, double 등의 표준 타입은 물론이고 구조체나 클래스 타입에 대해서도 동작해야 하다. tag_friend구조체 타입에 대한 PosValue 클래스를 작성하려면 이 타입에 대한 특수화 버전을 만들고 outValue 함수의 코드를 조금 다르게 작성할 필요가 있다.

특수화를 할 때는 다음 형식으로 클래스를 정의한다.

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

이렇게 정의하면 지정한 타입에 대해 특수화된 클래스를 생성한다. 인수의 타입이 이미 결정되어 있으므로 특수화된 클래스의 멤버 함수를 외부에서 정의할 때는 template<>을 붙이지 않아도 상관없다.

특수화를 하면 특수화된 클래스는 객체를 선언하지 않더라도 자동으로 구체화된다. 즉 클래스 정의가 만들어지고 멤버 함수들은 컴파일되어 실행 파일에 포함된다. 따라서 특수화된 클래스에 대한 정의는 일반적인 템플릿 클래스와는 달리 헤더 파일에 작성해서는 안되며 구현 파일에 작성해야 한다.

예제에서는 구조체에 대해서도 PosValue 템플릿을 쓰기 위해 특수화를 사용했는데 더 간단한 방법은 tag_friend 구조체가 << 연산자를 오버로딩해서 기존 템플릿의 본체 코드를 지원하는 것이다.

   

부분 특수화(Partial Specialization)란 템플릿 인수가 여러 개 있을 때 그 중 하나에 대해서만 특수화를 하는 기법이다.

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

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

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

some<int, double>이나 some<char, double>은 부분 특수화된 템플릿으로부터 생성될 것이다. 두 번째 인수가 double인 클래스에 대해서만 부분적으로 특수화를 했기 때문이다.

31.3 컨테이너

31.3.1 TDArray

컨테이너(Container)란 객체의 집합을 다룰 수 있는 객체이다. 쉽게 말해서 배열이나 연결 리스트 같은 것들을 컨테이너라고 하는데 동일한 타입(또는 호환되는 타입)의 객체들을 저장하며 이런 객체들을 관리할 수 있는 기능을 가지는 또 다른 객체이다.

  • 예제 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에 있던 ELETYPE 매크로는 사라졌으며 클래스 정의문 앞에 template<typename T>가 추가되었고 소스내의 모든 ELETYPE은 T로 대체했다. 이제 배열 요소의 타입은 매크로가 아닌 템플릿 인수에 의해 결정되며 객체를 선언할 때마다 원하는 타입을 지정할 수 있다.

멤버 함수의 소속은 모두 TDArray<T> 클래스가 되며 함수 본체의 ELETYPE은 T로 바뀐다. 기존 클래스가 템플릿화되면서 꼭 바뀌어야 하는 부분은 사실상 없는 셈이며 만약 있다면 그 클래스가 템플릿화를 할 만큼 충분히 일반화되지 못한 것이다.

  • 예제 TDArrayTest

#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 헤더 파일만 포함하면 템플릿이 정의된다.

정수나 실수에 대해서도 TDArray는 잘 동잦ㄱ한다.

TDArray.h 헤더파일만 포함시키고 객체를 선언할 때 원하는 타입만 밝히면 임의 타입에 대해 동작하는 동적 배열을 쉽게 사용할 수 있다. TDArray는 임의 타입에 대해서 잘 동작하는 배열이기는 하지만 모든 경우에 두루 쓸 수 있는 일반성을 갖추지는 못했다. 내부에서 동적 할당을 하므로 복사생성자와 대입 연산자를 원칙대로 적절히 정의해야 한다.

또한 동적 할당되는 포인터에 대한 배열이나 클래스에 대한 배열로 쓰기에는 조금 불편한 점이 있다. 포인터의 경우 삭제할 포인터가 가리키는 곳도 해제하는 것이 좋을 것이고 객체의 경우 생성자와 소멸자도 호출해 주면 편리하다.

31.3.2 TStack

  • 예제 iStack

#include <iostream>

   

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);

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());

}

동작에 필요한 필수 멤버들이 한 클래스에 캡슐화되어 있으므로 확실히 사용하기는 편리하다. 그러나 아직 타입에 대한 종속성을 해결하지 못했는데 타입만 변경될 뿐 알고리즘은 동일하므로 템플릿을 사용하면 임의 타입을 지원할 수 있다.

  • 예제 TStack.h

#include <iostream>

   

template<typename T>

class TStack

{

private:

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]; }

};

템플릿 기반으로 수정했으므로 int가 들어가야 할 위치에 T가 대신 들어갔다. 에러 처리를 위해 top 위치를 조사하는 GetTop 멤버 함수와 우선 순위 조사를 위해 지정한 위치의 값을 삭제하지는 않고 읽기만 하는 GetValue 함수가 추가되었다.

pop 함수의 에러 처리 코드가 삭제되었는데 임의의 타입에 대해 동작하기 위해서는 다른 방법이 필요하다.

  • 예제 TextCalcTemplate

#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;

   

const 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 ) ;

}

}

}

31.3.3 템플릿 중첩

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

  • 예제 NestTemplate

#include <iostream>

#include <windows.h>

using namespace std;

#include "TStack.h"

   

void gotoxy(int x, int y);

   

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();

}

   

void gotoxy(int x, int y)

{

COORD Pos={x,y};

SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),Pos);

}

TStack 클래스 템플릿과 PosValue 클래스 템플릿이 선언되어 있는 이 두 템플릿으로 임의의 타입에 대한 스택과 PosValue 객체를 만들 수 있다. 초기화되지 않은 객체를 만들 수 있도록 하기 위해 PosValue에 디폴트 생성자를 추가로 정의했다. sPos객체를 선언하고 있는데 TStack으로부터 만들어졌으므로 일단은 스택이다. 스택에 들어가는 요소는 인수열에 있는 PosValue<int> 타입이므로 이런 객체들의 임시 저장소가 된다.

컴파일러는 중첩된 선언문에서 안쪽 클래스부터 차례대로 구체화한다. 템플릿끼리 중첩되어 있을 뿐이지 별다른 사항은 없다.

C++은 템플릿의 중첩을 문법적으로 허가하므로 이중 삼중으로 템플릿을 중첩할 수도 있다. 그러나 문법과는 별개로 템플릿끼리 중첩되려면 두 클래스가 임의의 타입에 대해서도 잘 동작할 수 있도록 충분히 일반화되어 있어야 한다.

31.3.4 템플릿 클래스 인수

템플릿 클래스의 타입 명에는 항상 인수열이 같이 따라 다녀야 한다. 템플릿은 클래스를 만드는 선언문일 뿐이므로 그 자체가 타입이 될수는 없으며 인수를 밝혀야만 타입이 될 수 있다.

TStack<int> iS(10);

TStack<int> *piS;

TStack<char> *pcS;

piS = &iS;

pcS = &iS // 에러

타입이 다르기 때문에 억지로라도 대입하려면 캐스트 연산자를 사용한다.

pcS = (TStack<char>*)&iS;

  • 예제 TemplateTypePara

#include <iostream>

using namespace std;

#include "TStack.h"

   

void DumpStack(TStack<int> &s)

{

int i;

   

for( i = s.GetTop() ; i >= 0 ; i-- )

{

cout << i << "번째 = " << s.GetValue(i) << endl;

}

}

   

void main()

{

TStack<int> iS(10);

iS.push(1);

iS.push(2);

iS.push(3);

iS.push(4);

DumpStack(iS);

}

형식 인수열의 s에 대한 타입을 TStack<int> &로 밝히기만 하면 된다. 템플릿 클래스 타입을 인수로 받아들이는 함수는 사실 별 실용성이 없으며 이런 함수는 클래스의 멤버 함수가 되는 편이 훨씬 더 깔끔하다.

   

템플릿은 하나의 알고리즘을 여러 타입에 두루 사용할 수 있는 문법적인 장치이며 템플릿을 사용하면 한 번 만들어 놓은 코드를 타입에 상관없이 사용할 수 있는 이점이 있다. 그래서 템플릿은 코드의 재사용성을 극대화하는 도구로 사용되며 표준 템플릿 라이브러리의 문법적 기반이 된다. 템플릿을 사용하는 이런 프로그래밍 방법을 일반화(Generic)프로그래밍이라고 하며 객체 지향의 다음 세대로 지칭되기도 한다.

반응형

'책정리 > 혼자 연구하는 C,C++ 2' 카테고리의 다른 글

33장 타입 정보  (0) 2015.03.09
32장 예외 처리  (0) 2015.03.07
30장 다형성  (0) 2015.03.03
29장 상속  (0) 2015.02.28
28장 연산자 오버로딩  (0) 2015.02.27