읽는데 약 6분
함수 템플릿
함수 템플릿
함수 템플릿은 함수의 일반화 서술입니다. int나 double형과 같은 구체적인 데이터형을 포괄하는 일반형으로 함수를 정의합니다. 어떠한 데이터형을 템플릿에 매개변수로 전달하면 컴파일러는 그 데이터형에 맞는 함수를 생성합니다.
필요성
기존에 int 두 개를 더하는 함수를 작성했다고 합시다. 그리고 double형 두 개가 주어졌을 때 더하려고 합니다. 함수 오버로딩을 통해 매개변수 리스트를 달리하여 해결할 수도 있습니다. 하지만 이러한 상황이 반복적으로 일어난다면 함수 작성 도중 실수할 수도 있을뿐더러 시간 또한 낭비됩니다.
c++ 함수 템플릿 기능은 이 과정을 자동화해줍니다. 다음은 함수 템플릿을 설정하는 코드입니다.
template <class Any>
Any custom_add(Any a, Any b) {
return a + b;
}
class를 사용하는 대신 typename을 사용하여 작성할 수 도 있습니다.
template <typename Any>
Any custom_add(Any a, Any b) {
return a + b;
}
함수 템플릿은 함수를 미리 만드는 것이 아닙니다. 컴파일러에게 함수를 정의하는 방법을 알려주는 것입니다. 사용하는 인자에 따라서 그때마다 함수를 만들어 냅니다.
템플릿 오버로딩
다양한 데이터형에 대해 동일한 알고리즘을 수행할 때 템플릿이 유용한 것은 사실이지만 모든 데이터형에 대해 수행하는 알고리즘이 정확하게 일치하기는 어렵습니다. 이러한 문제를 해결하기 위해 함수 오버로딩과 마찬가지로 템플릿 또한 템플릿 오버로딩을 사용합니다. 템플릿 오버로딩도 함수 오버로딩과 마찬가지로 정확히 구분되는 시그니처를 사용해야합니다. 아래는 int형의 교환을 수행할 때와 배열의 교환을 수행하는 함수 템플릿의 원형과 정의입니다.
template <class any>
void SWAP(any& a, any& b) {
any temp;
temp = a;
a = b;
b = temp;
}
template <class any>
void SWAP(any* a,any* b,int size){
any temp;
for (int i = 0;i < size;++i) {
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
명시적 특수화
비슷한 알고리즘을 해결하기 위해 템플릿 오버로딩을 사용했습니다. 하지만 특정 데이터형을 위해 템플릿 오버로딩을 작성하기가 어려운 경우 또한 있습니다. 전달하는 매개변수가 정확히 일치하는 경우입니다. 즉 시그니처가 일치해 템플릿 오버로딩을 사용할 수 없는 경우입니다. C++는 이를 위해 명시적 특수화를 사용합니다. 말 그대로 특별한 경우를 처리하므로 일반적인 템플릿과 함께 사용합니다. 원형과 정의 앞에 template <>을 붙여 사용합니다. 다음은 DATA형에 대한 특수적 명시화입니다.
struct DATA {
int id;
char chr;
int val;
};
// 일반 함수 템플릿
template <class any>
void SWAP(any& a, any& b) {
any temp;
temp = a;
a = b;
b = temp;
}
// DATA형에 대한 명시적 특수화
template <> void SWAP<DATA>(DATA& a, DATA& b) {
char temp;
int temp2;
temp = a.chr;
a.chr = b.chr;
b.chr = temp;
temp2 = a.val;
a.val = b.val;
b.val = temp2;
}
이와 같이 DATA에서 id 값은 유지하고 나머지 값만 바꾸고 싶을 때 명시적 특수화를 통해 해결할 수 있습니다. 명시적 특수화는 자신이 특별히 판단해야 할 데이터형에 대한 정보를 이미 인자로 받았기 때문에 SWAP뒤에 있는 를 생략하여 아래와 같이 작성할 수도 있습니다.
template <> void SWAP(DATA& a, DATA& b) {
char temp;
int temp2;
temp = a.chr;
a.chr = b.chr;
b.chr = temp;
temp2 = a.val;
a.val = b.val;
b.val = temp2;
}
특수화와 구체화
템플릿을 잘 이해하기 위해 특수화와 구체화의 개념에 대해 명확히 이해할 필요가 있습니다. 위에서 언급했듯, 템플릿을 작성하면 컴파일러는 정의를 자동으로 생성하는 것이 아닙니다. 정의를 어떻게 작성할지 컴파일러에게 우리가 알려주는 것이 템플릿일 뿐이죠. 컴파일 도중 특정 데이터형에 대한 함수를 요구하면, 그제야 컴파일러는 해당 데이터형에 대한 함수 정의를 만들어 냅니다. 해당 함수의 정의를 만들 필요가 있다는 것을 암시적으로 컴파일러가 인지하기 때문에 이를 암시적 구체화라고 합니다.
컴파일러가 필요할 때, 해당 함수의 정의를 생성하는 암시적 구체화와는 달리 명시적 구체화는 사용자가 컴파일러에게 미리 해당 함수의 정의를 생성하도록 합니다.
정리를 하자면 템플릿을 보며 컴파일러가 자동으로 함수의 정의를 생성하는 것이 암시적 구체화, 사용자가 컴파일러에게 미리 함수의 정의를 만들 것을 명령하는 것이 명시적 구체화입니다. 이와 다르게 템플릿을 무시하고 특정 데이터형에 대해 직접 작성한 함수 정의를 사용하는 것이 명시적 특수화입니다.
명시적 구체화의 필요성
비슷하지만 다른 암시적 구체화와 명시적 구체화. 암시적 구체화만 있으면 되지, 왜 명시적 구체화라는 문법을 필요로 하는 걸까요? 이는 다음 코드를 보면 알 수 있습니다.
#include <iostream>
using namespace std;
template <class any>
any PLUS(any a, any b) {
return a + b;
}
int main() {
int a = 5;
double b = 3.0;
cout << PLUS<int>(a, b); //명시적 구체화 사용, PLUS(a,b)는 오류 발생
return 0;
}
이 템플릿은 PLUS(a, b)와 매치가 되지 않습니다. 왜냐하면 템플릿은 두 개의 매개변수의 데이터형이 일치할 것이라고 기대를 하기 때문입니다. 하지만 명시적 구체화를 통해 int 구체화를 강요하게 되고 매개변수 b는 int로 형 변환이 일어나기 때문에 코드가 작동합니다.
암시적 구체화, 명시적 구체화, 명시적 특수화를 통틀어 특수화라고 합니다. 특수화를 사용할 때는, 이름이 같은 템플릿 또는 템플릿이 아닌 함수와 동시에 사용하는 경우가 많습니다. 컴파일러는 이에 대해 우선순위를 다음과 같이 결정합니다. 실제로는 이보다 더 복잡하지만 큰 분기는 다음과 같습니다. 템플릿이 아닌 함수, 템플릿 함수, 명시적 특수화 함수가 주어졌다고 가정하겠습니다.
- 우선적으로 템플릿이 아닌 함수를 사용한다.
- 템플릿 함수보다 명시적 특수화 버전을 사용한다.
템플릿 선택
템플릿을 사용하지 않은 함수, 여러 템플릿 함수들이 공존할 때 컴파일러는 사용자를 대신하여 위와 같이 최적의 함수를 찾아주지만 사용자가 직접 사용할 함수를 선택할 수도 있습니다.
#include <iostream>
using namespace std;
template <class any>
any PLUS(any a, any b) { //#1
return a + b;
}
int PLUS(int a, int b) { //#2
return a + b;
}
int main() {
int a = 5;
int b = 4;
double c = 3.0;
double d = 2.0;
cout << PLUS(a,b) << '\n'; // #2 사용
cout << PLUS(c, d) << '\n'; // #1 사용
cout << PLUS<>(a, b) << '\n'; // #1 사용
cout << PLUS<int>(c, d) << '\n'; // #1 사용
return 0;
}
19번째 줄은 템플릿 함수와 템플릿이 아닌 함수와 매칭 되므로 우선순위가 높은 템플릿이 아닌 함수를 사용합니다.
20번째 줄은 매칭 되는 템플릿이 아닌 함수가 없으므로 템플릿을 보아 double형에 맞는 함수 정의를 생성합니다.
21번째 줄에서 <>의 의미는 템플릿이 아닌 함수 대신 템플릿 함수를 사용하라는 의미입니다. 즉 #1을 사용합니다.
22번째 줄은 명시적 구체화의 예시로써, #1을 사용합니다.
decltype
지금까지 살펴본 템플릿은 전달받은 매개변수의 타입이 모두 같은 경우였습니다. 다음과 같은 경우는 어떻게 될까요?
template <class T1,class T2>
void solve(T1 a, T2 b) { //#1
? type ? res = a + b;
...
}
?type? 부분은 전달받는 매개변수에 따라 값이 달라집니다. a가 int, b가 char 라면 ?type?은 정수승급에 따라 int형이되고 long long 과 int 라면 ?type?은 long long 형이 될 것입니다. 하지만 코드 내에서 모든 경우에 대해 처리를 하는 경우를 작성할 수는 없습니다.
이를 해결하기 위해 c++11부터 decltype 키워드를 도입해 해결했습니다.
template <class T1,class T2>
void solve(T1 a, T2 b) { //#1
//decltype(expression) var;
decltype(a + b) PlusType;
...
}
decltype이 구체적인 형을 결정하는 것은 다음의 순서를 따릅니다.
1. expression이 괄호가 없는 식별자라면 var은 식별자와 동일한 타입이 됩니다.
int a = 5;
double b = 5.0;
int& c = a;
const double d = 5.0;
decltype(a) A;//A는 int형이다
decltype(b) B;//B는 double형이다
decltype(c) C;//C는 int& 형이다
decltype(d) D;//D는 const double형이다
2. 만약 expression이 함수 호출일 경우 함수 리턴형 타입을 갖습니다.
3. expression이 lvalue일 경우 var은 expression 타입을 참조합니다. 1단계와 구분하기 위해 괄호를 사용합니다.
int a = 5;
decltype((a)) A; // A는 int& 형이다.
4. 앞 단계와 매칭이 되지 않았다면 var은 expression 타입과 동일합니다.
Trailng return type
decltype 또한 다음과 같은 한계가 있습니다.
template <class t1,class t2>
?type? solve(t1 a, t2 b) {
...
}
리턴을 위해 decltype을 사용하기 위해선 매개변수들이 선언이 되어야 합니다. 하지만 함수 리턴을 작성할 때는 아직 a, b들이 선언이 되어있지 않은 상태입니다. c++은 이를 해결하기 위해 Trailng return Type을 사용해 매개변수 선언 이후로 리턴 타입을 이동시킵니다. 즉 다음과 같이 작성하여 해결할 수 있습니다.
template <class t1,class t2>
auto solve(t1 a, t2 b) ->decltype(a+b) {
...
}