C, C++/C++ 언어 / / 2024. 10. 18.

[C++] 함수 템플릿의 특수화( function template specialization )

포괄적인 기능의 특수화

함수 템플릿은 모든 타입의 매개변수에 대하여 같은 기능을 수행하는 함수를 작성하는 도구입니다.

 

[C++] 함수 템플릿 ( function template )에 대한 설명 1

함수 템플릿 용도와 정의함수를 작성하다 보면, 매개변수의 타입만 다르고, 함수의 내용 자체는 중복되는 경우가 있습니다.다음 예가 그러한 함수의 하나일 것입니다.int add ( int a, int b){ return a +

codingembers.tistory.com

그렇기 때문에, 일반적으로 같은 함수 템플릿을 사용해 작성된, int 타입의 함수와 bool 타입의 함수의 기능이 크게 다를 수는 없습니다.

 

아래의 함수 템플릿은 매개변수인   val   의 타입과 값을 출력하는 print 함수를 정의하고 생성합니다.

// 매개변수의 타입과 값을 출력하는 함수 템플릿
template < typename T >
void print( const T& val){
    std::cout << typeid(val).name() << ": " <<  val << endl;
}

int main(){

    int n = 1;
    bool b = true;

    print<int>(n);  
    print<bool>(b);
}

▼출력

i: 1
b: 1

참고로, 위에서 사용한 typeid 함수는 매개변수   val   의 타입을 관한 정보를 알려주는 type_info 객체 참조를 반환해 줍니다.

그리고, 이 type_info 객체의 name는 타입을 알려주는 문자열을 반환해 주는 멤버 함수입니다.

이 함수가 반환한 문자열 "   i   "는 변수의 타입이 int 인 것을 말하고, "   b   "는 bool 타입을 알려주는 것입니다.

 

그런데, 이 함수 템플릿을 사용해서, int 타입과 bool 타입의 변수의 값을 출력해 보면, 그 결과를 구분할 수 없다는 것을 알 수 있습니다. ( 두 타입의 매개변수를 사용하는 함수 모두   1   을 출력 )

 

이런 문제가 생기는 것은 두 타입을 사용하는 함수의 정의가 동일하기 때문입니다.

따라서, 이 문제를 해결하려면 타입에 따라 다른 동작을 수행하는 함수를 만들면 될 것입니다.

 

가장 간단한 방법은 일반함수를 작성하는 것입니다.

template < typename T >	// 함수 템플릿
void print( const T& val){
    std::cout << typeid(val).name() << ": " <<  val << endl;
}

void print( bool bVal ){	// 일반 함수
    std::cout << typeid(bVal).name() << ": " \
     << std::boolalpha << bVal << endl;
}

int main(){

    int n = 1;
    bool b = true;

    print<int>(n);  
    print(b);
}

▼출력

i: 1
b: true

C++의 컴파일러는 일반 함수가 있다면, 템플릿 함수가 이미 생성되어 있더라도, 이 일반 함수를 더 선호합니다.

이것은 위의 글에서도 말했듯이, 일반 함수가 템플릿 함수보다 특수한 기능을 가질 가능성이 더 높다고 보는 것입니다.

그리고, 위에서도 일반 함수 print( bool ) 함수가 템플릿 함수보다 특수한 기능을 가졌다고 말할 수 있을 것입니다.

 

참고로, std::boolalpha는 값을 가진 변수가 아니라, 함수입니다.

이 함수는 bool 값을 "true" 또는 "false"로 출력할 것을 결정하는 내부 플래그( flags )를 설정합니다.

이 플래그가 설정된 이후 출력 스트림에 입력되는 bool 값은 숫자가 아니라 "true" 또는 "false" 문자열로 변환되어 출력됩니다.

그리고, 이 함수는 삽입 연산자   <<    연쇄 함수 호출이 가능하도록, 전달된 스트림 객체 참조를 다시 반환하도록 구현되어 있습니다.

 

다시 본론으로 돌아가, 특수한 기능을 수행하는 함수를 생성하는 두 번째 방법으로는, 함수 템플릿으로부터 명시적인 구체화 함수를 생성하는 것입니다.

위의 글에서 "암시적인 구체화"를 얘기하면서 다음과 같은 예를 들었습니다.

template < typename T >	// 기본(primary) 함수 템플릿
T add ( T a, T b){
    return a + b;
}

// 암시적으로 구현된 템플릿 함수 
// 실제론 눈에 보이지 않습니다.
template<>	
int add ( int a, int b){
    return a + b;
}

int main(){
    int ret = add( 3, 5 );
}

컴파일러가 main 함수의   add(3,5)   를 만나게 되면, 먼저 일반 함수가 있는지를 검사합니다.

그리고, 그다음 템플릿 함수가 있는지 검사하고, 이때도 함수를 발견할 수 없다면, 함수 템플릿으로부터( 이 템플릿이 있다면 ) 눈에 보이지 않는 템플릿 함수를 찍어냅니다.

이 과정이 암시적인 구체화( implicit instantiation )입니다.

그리고, 이때 사용된 함수 템플릿을 기본 함수 템플릿( primary function template )이라고 합니다.

 

컴파일러가 암시적인 구체화를 하는 방식과 같이, 기본 함수 템플릿에서 형식 템플릿 매개변수를 원하는 타입으로 대체하여, 구체적이고 눈에 보이는 함수를 생성하는 것을 명시적 템플릿 특수화( explicit template specialization ) 혹은 줄여서 템플릿 특수화라고 합니다.

 

사실, 컴파일러가 함수를 만들어내는 암시적인 구체화도 템플릿 특수화라고 하지만, 일반적으로는 구체화( instantiation )이라는 용어를 더 많이 사용합니다.

 

또한, 템플릿에서 모든 템플릿 매개변수가 특정한 타입으로 대체되는 특수화를 전체 특수화( full specialization )이라고 하고, 템플릿 매개변수의 일부만 특정한 타입으로 대체되는 것을 부분 특수화( partial specialization )이라고 합니다.

 

여기까지 얘기하면, 아래의 특수화된 함수 앞머리에 붙은   template <>   가 무엇을 말하는지 대충 감을 잡았을 거라 생각합니다.

template<>  // 전체 특수화 템플릿 함수 표시
int add ( int a, int b){
    return a + b;
}

이 템플릿 매개변수 선언( template parameter declaration )은 템플릿 함수가 기본 함수 템플릿을 사용해서 생성되었으며, 그러므로 일반함수가 아니라 템플릿 함수이고, 모든 템플릿 매개변수가 특수화( 전체 특수화 )되었음을 나타냅니다.

만일, 특정 템플릿 매개변수가 특수화되지 않았다면( 부분 특수화 ), 그 매개변수는   <>   안에 표시될 것입니다.

 

그리고, 이 특수화된 함수는 기본 함수 템플릿으로부터 생성돼야 하므로, 함수 템플릿이 선언된 후에야 구현할 수 있습니다.

또한, 함수 템플릿의 함수 정의와 그 형태가 일치해야 합니다.

template < typename T >	// primary 함수 템플릿
T add ( const T& a, const T& b){
    return a + b;
}

template<>  // const int& 매개변수 타입을 사용해야 함
int add ( const int& a, const int& b){
    return a + b;
}

 

이러한 특수화된 템플릿 함수에, 특정한 기능을 구현함으로써, 다른 타입의 구체화된 템플릿 함수와 다른 기능을 수행할 수 있습니다.

template < typename T >	// 포괄적 기능을 가진 함수 템플릿
void print( const T& val){
    std::cout << typeid(val).name() << ": " <<  val << endl;
}

template<>  // 특수한 기능을 가진 템플릿 함수
void print( const bool& val){
    std::cout << typeid(val).name() << ": " \
    << std::boolalpha << val << endl;
}

▼출력

i: 1
b: true

 

그렇지만, 일반 함수와 특수화된 템플릿 함수 중에 특별한 기능을 구현해야 한다면, 일반 함수를 선택하는 것이 타당합니다.

가장 중요한 것은 컴파일러의 선호도인데, 일반 함수와 템플릿 함수가 동시에 존재한다면, 반드시 일반 함수가 선택된다는 것입니다.

따라서, 템플릿 함수를 특별하게 만든다면, 보편적인 기능을 사용하기 위해선 일반 함수를 추가로 만들어야 합니다.

반대로, 일반 함수를 특별하게 만들면, 템플릿 함수는 다시 만들 필요가 없습니다.

만약, 템플릿 함수의 보편적인 기능이 필요한 경우, 함수 템플릿을 이용하여 컴파일러가 자동으로 생성할 것이기 때문입니다.

 

그리고, 템플릿 함수는 기본 함수 템플릿의 함수 정의와 같은 형태를 가져야 합니다.

template< typename T >
void doSomething( T obj){
    // do something
}

template<>  // 특수화된 함수
void doSomething( std::string obj){
    // do something
}

그러므로, 위의 특수화된 doSomething 함수는 std::string 타입의 객체를 값으로 전달받아야 합니다.

물론, 객체를 복사하는 연산을 피할 길이 없습니다.

 

그러나, 일반 함수는 이런 제약에 자유롭습니다.

void doSomething( const std::string& obj){	// 일반 함수
    // do something
}

일반 함수와 템플릿 함수는 오버로딩의 관계입니다.

즉, 함수 템플릿의 매개변수 타입을 반드시 준수할 필요가 없는 것입니다.

물론, 위와 같은 함수를 작성한다면, 객체 복사를 걱정할 필요가 없습니다.

 

정리하자면, 함수 템플릿이 있는 경우, 특별한 기능은 일반 함수를 통해서 구현하는 것이 타당하다는 것입니다.

 

비-형식 템플릿 매개변수를 가진 함수 템플릿의 특수화

이전 항목만 읽는 다면, 함수 템플릿의 특수화는 특정 타입의 매개변수를 가진 함수만을 작성하는 것으로 오해할 수도 있습니다.

함수 템플릿( 그리고, 클래스 템플릿 )은 형식( type ) 템플릿 매개변수는 물론 비-형식( non-type ) 템플릿 매개변수를 사용할 수 있고, 이 템플릿으로부터 특정 비-형식 템플릿 매개변수를 가진 함수를 특수화할 수 있습니다.

 

비-형식 템플릿 매개변수( non-type template parameter )에 관한 내용은 여기서 볼 수 있습니다.

 

[C++] 비-형식 템플릿 매개변수( non-type template parameter)

비-형식 템플릿 매개변수에 대한 설명이 글은 이전의 템플릿에 관한 글들과 연결되어 있습니다.시작하기 전에 '함수 템플릿 ( function template )에 대한 설명 1'을 읽어 보길 바랍니다. [C++] 함수 템

codingembers.tistory.com

 

아래의 예문에서 비-형식 템플릿 매개변수를 가진 함수 템플릿과, 이 템플릿에서 특수화한 함수를 볼 수 있습니다.

template < int N >  // 비-형식 템플릿 매개변수를 가진 함수 템플릿
void print(){
    std::cout << N << endl;
}

template<>  // 함수 템플릿의 특수화
void print<0>(){
    std::cout << "Zero\n";
}

int main(){
    print<5>();
    print<0>();
}

▼출력

5
Zero

위 예문에서는   print<0>   함수를 특수화함으로써, 템플릿 인수가   0   일 때는 다른 기능( 문자열 출력 )을 수행하도록 만들 수 있습니다.

 

그리고, 비-형식 템플릿 매개변수와 템플릿 특수화를 사용하여, 다음과 같은 재귀 함수 템플릿도 만들 수 있습니다.

template< int N >   // 재귀 함수 템플릿
int factorial(){
    return N * factorial<N-1>();
}

template<>  // 기저 조건의 특수화
int factorial<1>(){
    return 1;
}

int main(){

    int val = factorial<5>();
    cout << "factorial: " << val << endl;
}

▼출력

factorial: 120

위의 함수 템플릿은 비-형식 템플릿 매개변수를 이용하여 값 N의 factorial 값을 구하는 함수를 정의합니다.

그런데, 이런 재귀 함수를 작성하려면 기저 조건( base case ) 경우도 처리해야 합니다.

이것을 명시적 템플릿 특수화를 통해서 달성할 수 있습니다.

template<>  // 기저 조건의 특수화
int factorial<1>(){
    return 1;
}

위와 같은 특수화는 컴파일러가 자동으로 수행할 수 없으므로, 반드시 명시적 방법을 사용해야 합니다.

 

이러한 방식의 장점은 함수의 결과 값을 컴파일 시에 알 수 있다는 것과 중간 계산 결과 값이 저장된다는 것입니다.

( 함수를 특수화하는 과정에서 중간 계산 값들을 구하는 함수들이 생성됩니다. )

#include <array>

template< int N >   // 컴파일 시 상수 선언
constexpr int factorial(){
    return N * factorial<N-1>();
}

template<>  // 컴파일 시 상수 선언
constexpr int factorial<1>(){
    return 1;
}

int main(){

    constexpr int size = factorial<3>();	// 컴파일 시 상수
    cout << "array size: " << size << endl;

    std::array<int, size > arr;
}

위의 factorial <3>() 함수는 상수 표현식입니다.

따라서, 이 값으로 std::array 객체를 생성할 수도 있습니다.

 

하지만, 단점도 있습니다.

컴파일러는 값을 계산하기 위하여 재귀함수를 생성하고 실행 파일에 포함시키기 때문에, 컴파일 시간과 프로그램 실행 파일의 크기가 증가하게 됩니다.

 

이 글과 관련있는 글들

클래스 템플릿의 특수화( class template specialization )

템플릿의 부분 특수화( partial specialization )

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유