함수 템플릿 용도와 정의
함수를 작성하다 보면, 매개변수의 타입만 다르고, 함수의 내용 자체는 중복되는 경우가 있습니다.
다음 예가 그러한 함수의 하나일 것입니다.
int add ( int a, int b){
return a + b;
}
이러한 종류의 함수는 대부분 다른 타입의 매개변수를 사용해야 되는 경우가 발생합니다.
예를 들어, 위의 함수에 double 타입의 인수를 사용하려면, 어쩔 수 없이 다음과 같은 함수를 작성해야 됩니다.
double add ( double a, double b){
return a + b;
}
이 두 번째 버전의 함수는 매개변수와 반환 값의 타입만 다를 뿐, 함수의 내용은 완전히 동일합니다.
게다가, 만약 add 함수가 인수의 암시적인 변환을 해선 안 되는 함수라면( 예를 들면, 매개변수의 바이트 수에 따라 다른 기능을 수행해야 되는 함수 ), 추가로 char, float, short 버전의 함수도 작성할 필요가 생기게 될 것입니다.
심지어, int 타입 버전의 함수를 수정하게 되면, 이 변경 작업을 다른 모든 버전의 함수에도 똑같이 반영해야 됩니다.
이것은 명백하게 불합리한 작업 방식입니다.
이런 경우가 빈번하기 때문에, 한 번의 구현으로 모든 타입을 받아들일 수 있는 함수를 작성할 방법을 찾는 것은 아주 자연스러운 도전입니다.
그리고, C++에서는 함수 템플릿( function template )이라는 기능으로 이 문제를 해결하고 있습니다.
템플릿이란 이름이 알려주듯이, 함수 템플릿은 같은 모양의 함수를 찍어내는 도구입니다.
위 함수 add를 함수 템플릿으로 만든다면, 다음 과정을 거쳐야 합니다.
먼저, 매개변수의 타입과 반환 값의 타입에 임의의 타입을 사용해야 하므로, 이 임의의 타입이 들어가는 위치를 T 로 표시합니다.
그러면, 기존의 함수의 정의는 다음과 같이 변경됩니다.
T add ( T a, T b){
return a + b;
}
이때의 T 를 형식 템플릿 매개변수( type template parameter ) 혹은, 간단히 템플릿 형식( template type )이라고 하고, 이 형식 템플릿 매개변수 T 는 함수 템플릿에서 매개변수, 반환 값, 그리고 함수 내용에 사용되는 타입의 위치를 표시합니다.
즉, T 는 타입의 자리임을 알려주는 역할( placeholder )입니다.
그런데, 함수의 정의를 이렇게만 하면 컴파일러는 T 가 무엇인지를 알 수 없습니다.
그러므로, 이 T 가 int, double 등의 타입을 대변하는 임의의 형식임을 알려줘야 합니다.
그리고, 이 함수 정의가 템플릿 기능을 하는 함수라는 것도 알려야 할 필요가 있습니다.
그래야, 함수를 찍어 낼 필요가 있을 때, 컴파일러가 이 함수 정의를 사용하게 될 테니까요.
그래서, 필요한 것이 템플릿 매개변수 선언( template parameter declaration )입니다.
template < typename T > // 템플릿 매개변수 선언
여기서 template는 이 함수가 템플릿 기능을 한다는 것을 알려주는 키워드입니다.
그리고, 각 괄호 < > 안의 typename은 T 가 임의의 타입을 표현하는 것을 나타냅니다.
typename 대신에 class 키워드를 사용할 수도 있는데, 이 class는 typename를 사용하기 전부터 사용해 오던 키워드로, 이 둘의 의미는 동일합니다.
그럼에도 typename 키워드를 추가한 이유는, 클래스나 구조체 같은 타입뿐만 아니라, int, char 같은 원시 타입도 T 로 표현한다는 것을 강조하기 위한 것입니다.
그리고, 이러한 템플릿 매개변수 선언과 함수 템플릿의 정의를 합친 것이 함수 템플릿( function template )입니다.
template < typename T > // 템플릿 매개변수 선언
T add ( T a, T b ){ // 함수 템플릿 정의
return a + b;
}
이때, 형식 템플릿 매개변수 T 의 범위( scope )는 템플릿 매개변수 선언이 속한 함수 템플릿으로 한정됩니다.
따라서, 다른 함수 템플릿에서 같은 이름의 템플릿 매개변수( 여기선 T )를 다시 사용하더라도, 이름 충동은 발생하지 않습니다.
참고로, 이 형식 템플릿 매개변수의 이름을 반드시 T 로 할 필요는 없습니다.
그럼에도, 기존의 코드들을 보면, 형식 템플릿 매개변수의 이름이 대부분 T 로 된 것을 볼 수 있습니다.
이것은 T 가 template의 약자이기도 하고, 많은 사람들이 묵시적으로 이렇게 사용해 왔기 때문입니다.
함수 템플릿의 사용
이렇게 만든 함수 템플릿을 사용하려면 다음과 같이 할 수 있습니다.
int main(){
int iRet = add<int>(5, 3);
double dRet = add<double>(3.14, 2.9);
cout << iRet << " " << dRet << endl;
}
▼출력
8 6.04
여기서, 함수명 add와 같이 사용된 <int> 와 <double> 을 템플릿 인수( template argument )라고 합니다.
그리고, 컴파일러는 add<int>(5,3) 같이 템플릿 함수를 호출하는 코드를 보게 되면, 이러한 템플릿 인수의 정보를 사용하여, int 타입의 매개 변수를 가진 실제 add 함수를 작성합니다.
이러한 과정을 함수 템플릿 구체화( function template instantiation )라고 합니다.
이렇게 구체화된 함수는 우리의 눈에는 보이지 않기 때문에, 암시적인 구체화( implicit instantiation )라고도 합니다.
이 구체화된 함수의 실제 코드는 다음과 같습니다.
template<> // 함수 템플릿에서 구체화되었음을 알림
int add( int a, int b){
return a + b;
}
이 함수가 전체 프로그램에 포함되고, 보통의 함수와 똑같은 방법으로 호출됩니다.
만약, 구체화된 함수가 이미 작성되었다면, 컴파일러는 더 이상 함수를 따로 작성하지 않고, 이미 작성된 함수를 사용하게 될 것입니다.
마찬가지로, add<double>(3.14, 2.9) 함수를 호출하면, 함수 템플릿을 이용해서 다음 함수를 생성합니다.
template<>
double add( double a, double b){
return a + b;
}
이 함수는 int 타입의 add 함수의 오버로딩( overloading ) 함수가 됩니다.
즉, 함수 템플릿을 사용한다고 해서, 필요한 함수의 개수가 줄어드는 것이 아니라, 반복되는 노동을 제거해 주는 것이 템플릿의 역할입니다.
만약, 함수 템플릿의 함수 정의를 수정하면, 이 템플릿을 통해서 생성된 모든 함수는 수정된 함수 정의를 갖게 됩니다.
마지막으로, 함수들을 만들 때 사용된 함수 템플릿은 컴파일이 종료되면 더 이상 필요가 없어지게 됩니다.
( 당연히 프로그램에 포함되지도 않습니다. )
함수 삭제 ( = delete )
함수 템플릿에서 형식 템플릿 매개변수 T 의 자리엔 어떤 타입이든 사용될 수 있습니다.
그래서, 아래의 add 함수 템플릿 통해서 std::string 타입의 매개 변수를 가진 템플릿 함수를 구체화할 수도 있습니다.
template < typename T > // 함수 템플릿
T add ( T a, T b ){
return a + b;
}
int main(){
std::string str1 = "Hello, ";
std::string str2 = "Welcome !!";
// string 타입의 함수 구체화
std::string ret = add< std::string >( str1, str2 );
cout << ret << endl;
}
▼출력
Hello, Welcome !!
그런데, 어떤 경우엔 이러한 std::string 타입의 매개 변수를 add 함수에 전달할 수 없도록 하고 싶을 때도 있을 수 있습니다.
그럴 때 사용할 수 있는 것이 함수 삭제입니다.
template<>
string add( string, string ) = delete; // 명시적인 함수 삭제
int main(){
string str1 = "Hello, ";
string str2 = "Welcome !!";
string ret = add< string >( str1, str2 ); // error !!
cout << ret << endl;
}
▼출력
error: use of deleted function 'T add(T, T) [with T = std::__cxx11::basic_string<char>]'
위와 같이, string 버전의 템플릿 함수를 = delete 와 같이 사용함으로써, 이 함수를 명시적으로 제한할 수 있게 됩니다.
만약, 이런 함수를 호출하게 되면, 위 출력과 같이 삭제된 함수를 사용하려고 한다라는 오류와 만나게 될 것입니다.
이러한 함수 삭제는 템플릿 함수뿐만 아니라 일반 함수, 클래스의 멤버 함수를 금지시키기 위해 사용되기도 합니다.
이것에 관한 내용은 여기서 볼 수 있습니다.
형식 템플릿 매개변수 추론( type template parameter deduction )
함수 템플릿의 매개변수를 명시하지 않더라도, 컴파일러는 주어진 함수의 인수들로부터 형식 템플릿 매개변수를 추론해서 필요한 함수를 구체화( instantiation )합니다.
template < typename T > // 함수 템플릿
T add ( T a, T b){
a += b;
return a;
}
예를 들어, 위와 같은 함수 템플릿이 있는 경우에, add( 5, 3 ) 함수를 호출하면, 컴파일러는 함수의 인수들( 여기서는 5와 3 )로부터 형식 템플릿 매개변수인 T 를 int 타입으로 추론합니다.
따라서, add<int>(5, 3) 같이 함수를 호출하지 않아도, 필요한 함수를 만들고 호출하게 되는 것입니다.
int iRet = add( 5, 3 ); // 8
그럼, int 타입 대신 int 참조 타입( int& )의 인수를 사용하면, 컴파일러는 어떤 형태의 add 함수를 만들어 낼까요?
int main(){
int a = 10;
int b = 20;
int& ra = a; // 참조 타입
int& rb = b;
int iRet = add( ra, rb ); // 템플릿 함수 구체화
cout << "a: " << a << ", iRet: " << iRet << endl;
}
형식 템플릿 매개변수 T 를 int& 타입으로 대체할 수 있으므로 다음과 같은 함수가 생성될 것을 예상할 수 있습니다.
template<> // int& 타입의 매개변수를 사용하는 add ?
int& add( int& a, int& b){
a += b;
return a;
}
그렇지만, 컴파일러는 예상과 달리 이러한 형태의 함수를 만들어내지 않습니다.
실제로 만들어 내는 함수는 다음과 같습니다.
template<> // int 타입의 매개변수를 사용하는 add
int add( int a, int b){
a += b;
return a;
}
▼출력
a: 10, iRet: 30
따라서, main 함수 안의 변수 a 의 값은 전혀 변경되지 않습니다.
template < typename T > // 형식 템플릿 매개변수
T add ( T a, T b){ // 함수 매개변수 타입
a += b;
return a;
}
이것은 위와 같이 형식 템플릿 매개변수 T 와 함수 매개변수 타입이 같은 경우, 컴파일러는 인수의 타입( 여기서는 int& )에서 참조 속성과 상수 속성을 무시한 타입을 T 의 타입으로 추론하기 때문입니다.
( 따라서, 여기서는 T 를 int 타입으로 추론 )
컴파일러가 암시적으로 만드는 함수는 눈에 보이지 않기 때문에, 이렇게 예상과 다른 결과를 가져오는 템플릿을 작성할 수 있습니다.
그러므로, C++에서 함수 템플릿을 통해서 함수를 구체화하는 방식을 정해둔 규칙은, 한번 시간을 들여 익혀두면 꽤 도움이 됩니다.
이러한 C++의 템플릿 형식 추론에 관한 내용은 여기서 볼 수 있습니다.
원래 의도했던, int& 타입의 매개변수를 받아들이는 함수를 생성하는 템플릿의 형태는 다음과 같습니다.
// int& 타입의 매개변수를 사용하는 함수를 생성하는 함수 템플릿
template < typename T >
T add ( T& a, T& b){
a += b;
return a;
}
▼출력
a: 30, iRet: 30
템플릿 함수와 일반 함수의 관계
그런데, 다음과 같이 함수 템플릿과 일반 함수가 동시에 구현되어 있으면 어떻게 될까요?
template < typename T >
T add ( T a, T b){
a += b;
return a;
}
template<>
int add ( int a, int b ){ // 템플릿 함수
cout << "template function\n";
a += b;
return a;
}
int add ( int a, int b){ // 일반 함수
cout << "normal function\n";
a += b;
return a;
}
이때, 먼저 알아야 할 것은, C++에서는 함수 템플릿에서 구체화된 템플릿 함수 add와 일반 함수인 add를 같은 함수로 보지 않고, 오버로딩( overloading )된 함수로 본다는 것입니다.
따라서, 위의 코드들은 중복된 함수 이름과 정의로 인한 오류를 발생시키지 않습니다.
그럼, 다음과 같은 함수들이 호출될 때, 컴파일러는 어떤 함수를 사용하게 될까요?
int main(){
int iRet1 = add<int>(5,3); // call template function
int iRet2 = add(5, 3); // call normal function
int iRet3 = add<>(5, 3); // call template function
}
main 함수의 첫 번째 줄은 템플릿 인수 <int> 를 명시하고 있으므로, 이 경우엔 템플릿 함수를 호출합니다.
만약, 템플릿 함수가 구체화되어 있지 않다면, 함수를 구체화하고, 그 함수를 호출할 것입니다.
그리고, 두 번째 줄은 템플릿 함수와 일반 함수가 있어도, 컴파일러는 일반 함수를 선호합니다.
이것은 일반 함수를, 포괄적인( generic ) 목적의 템플릿 함수의 특수 버전이라고 간주하기 때문입니다.
함수 템플릿은 여러 타입의 함수를 만들어 낼 수 있습니다.
따라서, 함수의 내용에 특수한 기능을 수행하는 코드를 작성하기보다는, 모든 타입이 만족할만한 기능을 작성하는 것이 일반적입니다.
그런 면에서, 템플릿 함수와 일반 함수에 동시에 접근이 가능하다는 것은, 일반 함수에 특별한 기능을 넣어두었다고 간주하는 것이 타당하다고 바라보는 것입니다.
그런데, "이런 특수한 일반 함수 대신 템플릿 함수를 호출하고 싶다면 어떻게 해야 하나?"하고 궁금해 할 수도 있습니다.
그때 쓸 수 있는 방법이 세 번째 문장입니다.
이 함수 호출은 컴파일러가 템플릿 함수만을 호출하도록 지시합니다.( 물론, 첫 번째 문장도 가능합니다. )
그리고, 인수들로부터 형식 템플릿 매개변수를 추론하므로, 명시적으로 표시해야 할 타입을 신경 쓸 필요가 없습니다.
이 글은 다음 글까지 이어집니다.
이 글과 관련있는 글들
클래스 템플릿( class template )에 대한 설명
비-형식 템플릿 매개변수( non-type template parameter)
함수 템플릿의 특수화( function template specialization )
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 클래스 템플릿( class template )에 대한 설명 (0) | 2024.10.15 |
---|---|
[C++] 함수 템플릿( function template )에 대한 설명 2 (0) | 2024.10.13 |
[C++] 포인터 this의 이해 (2) | 2024.10.07 |
[C++] 함수 삭제( = delete )를 통한 기능 제어 (1) | 2024.10.05 |
[C++] 사용자 정의 타입( user defined type )을 다른 타입으로 변경하기 (0) | 2024.10.01 |