개요

Template 은 말 그대로 지정되지 않은 타입을 나타내는 것이다. 그게 전부다 (…) 정 헷갈리면 위키피디아의 정의를 보자.

템플릿(template)은 C++ 프로그래밍 언어의 한 기능으로, 함수와 클래스가 제네릭 형과 동작할 수 있게 도와 준다. 함수나 클래스가 개별적으로 다시 작성하지 않고도 각기 다른 수많은 자료형에서 동작할 수 있게 한다. 이는 튜링 완전 언어로 볼 수 있다.

템플릿은 C++에서 프로그래머들에게 유용한데, 특히 다중 상속과 연산자 오버로딩과 결합할 때 그러하다. C++ 표준 라이브러리는 연결된 템플릿의 프레임워크 안에서 수많은 유용한 함수들을 제공한다.

기본 예제

예제가 필요하다. 이 코드는 작동이 가능하다.

#include <iostream>
using namespace std;

int myFunc(int a, int b) {
    return a + b;
}
double myFunc(double a, double b) {
    return a + b;
}
int main () {
    cout << myFunc(1, 3) << endl;
    cout << myFunc(1.3, 3.2) << endl;
    system("pause");
    return 0;
}

그런데 함수가 반복된다. 저 myFunc() 를 하나로 쓰면 좋을텐데!

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

이렇게 줄여 쓸 수 있다는 것이다. 와우!

하지만 함정이 있다. myFunc(1, 3.2) 으로 호출하면 에러가 난다. int 와 double 이 혼용되어 있는데, 템플릿 함수의 인자는 모두 같은 자료형 T 를 받아야 하기 때문이다.

E0304 인수 목록이 일치하는 함수 템플릿 “myFunc”의 인스턴스가 없습니다.

각각 나타내려면, 이렇게 해야 한다. (return 형은 임의로 T2 로 했다. 별 의미 없다.)

template <typename T1, typename T2> 
T2 myFunc(T1 a, T2 b) {
    return a + b;
}

특수화 (= 오버라이딩)

템플릿 함수 안에서, 특정 타입에 대해서는 다른 행동을 하고 싶을 경우가 있다. 이 코드를 보자.

template <typename T> // 함수 템플릿 정의
int SizeOf(T a)
{
    return sizeof(a);
}

int main(void)
{
    int i = 10;
    double e = 7.7;
    const char* str = "Good morning!";

    cout << SizeOf(i) << endl;
    cout << SizeOf(e) << endl;
    cout << SizeOf(str) << endl;
    system("pause");

    return 0;
}

64비트 환경에서 이 코드의 결과를 맞춰보자. 그냥 SizeOf() 대신 sizeof() 를 호출했다고 가정하면 된다. 즉, 차례대로 4, 8, 8 이다. 마지막은 왜 Good morning! 의 길이인 13이 아니라 8이지? 라고 생각한다면 C 공부를 다시 해야 한다.

그럼, 정말 원하는 대로 char* 인 경우에는 문자열 길이를 반환하는 함수를 만드려면 어떻게 해야 할까? 답은 아주 간단하다. 템플릿 함수건 아니건 간에 ‘함수 오버라이딩’ 은 언제나 가능하다.

다음 함수를 추가한다.

int SizeOf(const char * a)
{
    return strlen(a);
}

그런데 예제는 다 이렇게 나와 있다.

template<>
int SizeOf(const char * a)
{
    return strlen(a);
}

이건 일종의 약속이지 않을까? “템플릿 함수를 오버라이딩 했어요. 그러니까 잘 봐주세요” 같은 건데.. 아무튼 둘 다 가능하니 알아두자. 이게 왜 의미가 있냐면, 아래 ‘구체화’ 파트에서 다시 이야기한다.

그리고 발견한게 하나 더 있는데, 이러면 안 된다. 키워드까지 모두 일치해야 한다. 이건 템플릿도 그렇고 오버라이딩도 그렇고 알고 있어야 한다.

template<>
int SizeOf(char * a) // 그냥 char*
{
    return strlen(a);
}
int main(void)
{
    const char* str = "Good morning!";
    // 이러면 const char* 이므로 오버라이딩 된 함수가 호출되지 않는다!!
    cout << SizeOf(str) << endl; 
    system("pause");
    return 0;
}

구체화

템플릿 함수가 있는 코드를 컴파일하면, 템플릿 함수가 타입별로 촤르륵 풀리게 된다. 이걸 굳이 이름을 붙여놨다. ‘구체화’ 라고… 아무튼 출처를 참고해서 요약한 내용은 다음과 같다.

이 함수는 이름은 같고 타입은 다르.. 아니 잠깐만. 이거 그냥 함수 오버라이딩 아닙니까? 그렇다. 템플릿은 함수 오버라이딩을 반복적으로 할 개발자를 배려한 매우 강려크한 기능 되시겠다.

명시적 타입 지정

자, 여기서 궁금한 것 하나. 첫 예제에서 myFunc(1, 3.2) 를 호출하면 에러가 났다는 것을 상기하자. 여기서, 사용자는 1을 3.2 와 같은 타입이라고 컴파일러에게 알리고 싶다.

방법은 두 가지.

둘 중 어느 방법을 써도 map 파일의 내용은 변함이 없다는 점을 꼭 기억하자. 구체화 때에 이야기했지만, 참조되는 템플릿 함수만 구체화된다. 캐스팅을 인자에 하던 템플릿 함수 타입을 강제하건 간에 참조되는 인자들의 타입은 정해지는 것이기 때문이다.

T 의 종류 (?)

template <typename T>
template <class T>

클래스도 있다는 사실만 짚고 다음으로 넘어간다.

클래스 템플릿

백문이 불여일견, 먼저 아래 코드를 보자.

template <class T>
class Person {
    string name;
    T height;
public:
    Person(string aName, T aHeight) : name(aName), height(aHeight) {}
    int getHeight() {...};
};

일단 희한하게 생긴 생성자는 신경쓰지 말자. height 는 뭐가 될지 모른다. 임의로 만든 Height 가 될 수도, Int 가 될 수도 있다.

개념은 여기까지만 이해하면 되지만, 코딩할 때는 하나 더 알아야 할 사실이 있다. 보통은 클래스는 헤더 파일에 정의만 하고 구현부는 소스 파일에 넣기 때문에 이런 식으로 구현한다.

class Person {
    string name;
    int height;
public:
    Person(string aName, int aHeight) : name(aName), height(aHeight) {}
    int getHeight();
};
int Person::getHeight() {
    return this->height;
}

그런데 만약 클래스 템플릿인 경우라면 조금 귀찮지만 템플릿을 모두 지정해야 한다.

template <class T>
class Person {
    string name;
    T height;
public:
    Person(string aName, T aHeight) : name(aName), height(aHeight) {}
    T getHeight();
};
template <class T>
T Person<T>::getHeight() {
    return this->height;
}

보이는가? 세 부분이 추가되었는데,

자, 그럼 이렇게 ‘안 하면’ 어떻게 될까? Full Code 를 가져와 본다.

#include <iostream>
#include <string>

using namespace std;

template <class T>
class Person {
    string name;
    T height;
public:
    Person(string aName, T aHeight) : name(aName), height(aHeight) {}
    T getHeight();
};

template <class T>
T Person<T>::getHeight() {
    return this->height;
}

int main ()
{
    string sHeight = "100";
    Person<string> sHuman("Ryan", sHeight);

    cout << sHuman.getHeight() << endl;
	system("pause");

	return 0;
}

이건 시사하는 바가 있다. 2개의 다른 T 를 참조해서 Person 객체를 만든 다음, 컴파일해서 맵 파일을 열어보면 Person 의 클래스 기본 함수도 2개고 getHeight() 함수도 2개다. 구체화가 2개가 됐다는 이야긴데, 이것들도 전부 ‘Person’ 이라는 이름은 아닌 것이다.