프로그래밍/cpp

C++ 템플릿(template) 이해하기

콘파냐 2017. 4. 18. 22:47
반응형

템플릿을 사용하여 프로그래밍 하는 것을 일반화(Generic) 프로그래밍이라고도 한다. 개인적으로 C++을 제대로 공부하기 위한 첫 번째 관문은 템플릿(template) 아닐까 생각한다. 템플릿을 알아야 STL(Standard Template Library)를 공부할 수 있기 때문에 이 관문을 거쳐가야 한다. 물론 템플릿을 제대로 이해하는 것이 만만한 영역은 아니지만 STL은 사용방법만 알면 깊은 이해없이도 기본적인 사용은 할 수 있다. 어찌되었건 템플릿을 제대로 공부하려면 큰 맘을 먹어야 한다.

여기에서는 템플릿의 기본적인 내용을 우선 정리해보려고 한다.


그렇다면 템플릿은 무엇인가?

식상하지만 많은 책에서 붕어빵 틀, 또는 모형 자 등으로 비유된다. 붕어빵 틀에 밀가루 반죽을 부어서 붕어빵을 찍으면 붕어빵이 나온다. 같은 맥락으로 C++에서의 템플릿은 함수나 클래스를 만들 수 있는 틀이라고 생각하면 된다. 따라서 함수나 클래스가 붕어빵인 셈이다.


일반화라는 말의 의미는 붕어빵 틀을 만드는 것과 같다. 이런 틀이 없다면 붕어빵을 손으로 빗어서 만들어야 할 것이지만 이렇게 특정 기능을 하는 틀을 만들어 놓으면 어떤 재료를 넣든 붕어빵 모양의 결과물이 나온다. 이 때 재료는 C++에서 객체의 타입에 대응된다.


대략 이런느낌?템플릿

▲그림같이 찍어내는 모양은 같지만 타입에 따라서 색은 달라진다. 여기서 색은 객체의 타입에 대응된다. 이미 알고있는 것처럼 템플릿을 사용하지 않고 여러 타입에 대해서 동작하는 함수를 만들려면 각 타입에 대해 함수를 오버로딩을 해야 할 것이다.

하지만 하나의 템플릿만으로 함수를 오버로딩하는 효과를 가질 수 있다면 이것이 일반화 프로그래밍의 목표라 할 수 있다.


이렇게 다양한 타입을 고려하다보면 타입이 다를 때 특정 타입에 대해서는 붕어빵이 아닌 별빵 또는 다른 기능을 수행하도록 할 필요도 있다. 이를 템플릿 특수화라고 한다.

▲ 템플릿 특수화도 함수 오버로딩과 비슷하게 이해할 수 있다. 특수화를 하려는 타입에 대한 템플릿을 따로 하나 더 정의하면 된다. 위 그림은 C타입에 대해서 동작하는 템플릿을 따로 만든 것이다.



템플릿 구체화


기본적인 템플릿 문법을 살펴보도록 하자.

다음은 아주 간단한 템플릿에 대한 예제코드다.

템플릿을 만들기 위해서는 템플릿 선언이 필요하다. 

template <typename T>


T라는 타입에 대해 템플릿을 선언한다는 뜻이다. 여러 타입에 대한 템플릿을 만들고 싶다면 다음과 같이 선언하면 된다.

template <typename T1, typename T2, ...>


T는 모든 타입을 대변하는 이름이다. 사용자가 해당 템플릿을 사용하려면 T타입으로 선언된 위치에 값이나 변수를 넣어서 템플릿을 호출하면 대입된 타입이 컴파일 시간에 자동으로 타입이 추론되어 해당 타입에 맞는 함수 또는 클래스가 구체화 된다. 


myFunc(1, 3)

위 코드로 인해 컴파일 타임에 int myFunc(int, int)의 함수가 구체화 되는 것이다. 


myFunc(1.3, 3.2)

같은 방식으로 컴파일 시에 위 코드를 만나면 double myFunc(double, double)의 함수가 구체화 된다. 


이렇게 템플릿의 구체화는 컴파일 타임에 필요한 타입에 대해서만 구체화를 하게된다. 위 코드에서 구체화 된 두 개의 함수는 함수이름은 같고 시그니처만 다르므로 서로 오버로딩 관계다. 색이 다른 붕어빵인 것이다.


템플릿 구체화에서 생각해야할 문제


모듈화 프로그래밍에서 헤더파일(.h)과 cpp파일에 함수 또는 클래스의 선언과 정의를 따로 하게된다. 이렇게 하는 이유는 실행파일과 모듈의 분리, 또는 구체적인 구현을 숨기려는 목적도 있겠고 모듈을 미리 컴파일 하여 매번 불필요한 컴파일을 하지 않으려는 의도도 있을 것이다. 

어찌되었건 일반적으로 헤더파일에는 선언만 하지 정의를 하지 않는다. 하지만 템플릿은 이런 방법으로 선언과 정의를 따로 하게되면 문제가 발생한다.


컴파일 타임컴파일 타임

위 그림은 컴파일 타임에 소스가 번역되는 단위를 간략히 보여주는 그림이다. 번역단위1과 번역단위2는 컴파일 타임에 서로를 참조하지 않는다. 각각 따로따로 컴파일이 되어 목적파일로 된다. 보통은 a.h에는 a.cpp에서 정의된 함수나 클래스에 대한 (extern)선언을 하기 때문에 컴파일 타임에 아무런 문제가 발생하지 않는 것이다. 구체적인 구현에 대한 연결은 링크과정에서 링커에게 맏기게 되어있기 때문이다.


하지만 템플릿의 경우는 구체화라는 작업을 컴파일 타임에 하기때문에 구체적인 구현이 다른 번역단위에 있다면 문제가 되는 것이다. 즉 템플릿의 구체화를 위해서는 템플릿 정의가 같은 번역단위에 있어야 구체화가 가능하다.(함수나 클래스의 정의부가 없다면 구체화를 할 수 없으므로)  따라서 a.h에 템플릿 선언, a.cpp에 템플릿 정의, 이런 방식은 구체화가 되는 시점(컴파일 타임)에서 컴파일 에러가 발생한다.

결론은 템플릿은 어쩔 수 없이 헤더파일에 선언과 정의가 같이 있어야 한다.


템플릿 구체화 지정


위에서 설명한데로 템플릿의 구체화는 컴파일 타임에 템플릿 함수가 호출될 때 자동으로 이루어진다. 전달된 타입에 대한 추론은 컴파일러가 알아서 하는데 몇가지 경우에 구체화될 함수의 타입을 강제로 지정해야할 경우가 있다. 


예1) 

myFunc(1.4, 8)

1.4와 8이 서로 다른 타입이라고 볼 수 있을까? 왜냐면 일반적인 변수 선언 double x  = 8; 이라고 하는 것은 에러가 아니기 때문이다. 암묵적으로 8이 double 형으로 바뀔 수 있기 때문이다. 하지만 템플릿을 사용할 때 이런 암묵적인 변환은 허용되지 않는다. 리터럴 그대로의 타입(8은 int형 리터럴, 1.4는 double형 리터럴)으로 해석된다. 이런 경우를 위해 사용자가 직접 타입을 강제하는 문법이 마련되어 있다.


      myFunc<double>(1.4, 8); //9.4반환

myFunc<int>(1.4, 8); //9 반환  

함수가 컴파일러에 해석될 때 인수를 추론하기 전에 미리 인수에 대한 타입을 강제하는 것이다. 이 경우 타입추론 대신 해당 데이터를 형변환하게 된다.


예2) 반환 타입에 대한 추론은 컴파일러가 할 수 없다.


앞의 예에서 컴파일러는 코드에서 myFunc(1, 2)를 발견하면 인수에 대한 타입추론을 하게 된다. 위 코드의 경우 이렇게 추론된 타입은 반환 타입과 같으므로 별 문제가 없다. 하지만 템플릿이 인수와 반환타입을 각각 따로 다룬다면 코드상에서는 반환 타입을 알 수가 없으므로 곤란하게 된다. 보통 이런 경우는 사용자가 반환타입을 명시적으로 지정하는 목적으로 설계가 된다. 예를들어 타입을 캐스팅 하는 함수를 만든다면 사용자가 반환타입을 직접 지정해야할 것이다.

템플릿 특수화


앞서 언급한 바와 같이 특수화(specialization)는 특정 타입에 대해서 따로 템플릿을 정의하는 것을 말한다. 


예)

위 코드는 char* 타입에 대해서 특수화를 한 코드다. char*타입의 경우 두 문자열을 연결해 주는 기능을 하도록 특수화 하였다.

template <>

char* myFunc<char*>(char* a, char* b)

위 코드는 템플릿을 특수화를 선언하는 기본적인 방법이다. 함수명과 함수 시그니처 사이에 <char*>은 타입 T에 대응되는 타입을 명시적으로 밝힌 것인데 이를 생략해도 된다. 하지만 써주는 것을 권장한다.


템플릿의 구체화와 특수화에 대해서 대략적으로 살펴보았는데 앞으로도 살펴볼 내용은 많이 있다. 템플릿을 설계하지 않더라도 소스코드를 해석하기 위해서도 잘 알아두어야한다. 템플릿 관련 에러는 좀 복잡하게 뜨는데 대체로 에러의 원인을 파악하기도 힘들다. 템플릿을 어설프게 이해했다면 더욱 더 에러를 고치기 힘들 것이다. 아무튼 템플릿 관련 내용은 C++11, C++17에도 늘어났기 때문에 부가적인 내용을 꾸준히 공부할 필요가 있다.

반응형