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

[C++] 템플릿의 부분 특수화( partial specialization )

함수 템플릿의 부분 특수화

기본 템플릿( primary template )에서 템플릿 매개변수를 특정한 타입이나 값을 표현하는 식으로 제한하여, 특수한 기능을 구현하는 것을 템플릿의 특수화( template specialization )라고 했습니다.

 

이러한 템플릿 특수화에 대한 글은 여기에서 볼 수 있습니다.

 

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

포괄적인 기능의 특수화함수 템플릿은 모든 타입의 매개변수에 대하여 같은 기능을 수행하는 함수를 작성하는 도구입니다. [C++] 함수 템플릿 ( function template )에 대한 설명 1함수 템플릿 용도와

codingembers.tistory.com

 

아래는 임의의 타입의 데이터를 임의의 횟수만큼 출력하는 함수 템플릿입니다.

#include <iostream>
using namespace std;

template < typename T, int N >  // 기본 함수 템플릿
void print( T data ){
    for( int i = 0; i < N; i++){
        cout << data << " ";
    }
}

int main(){
    print<int, 3>( 5 );
    print<bool, 3>( true );
}

▼출력

5 5 5 1 1 1

 

그리고, 위의 템플릿에서   bool   타입의 데이터만 특별한 방식으로 출력하려면, 다음과 같이 할 수 있습니다.

template<>  // 전체 특수화
void print<bool, 3>( bool data){
    for( int i = 0; i < 3; i++){
        cout << boolalpha << data << " ";
    }
}

// main 함수는 위와 동일

▼출력

5 5 5 true true true

그런데, 위의 함수 템플릿은 기본 함수 템플릿으로부터 모든 템플릿 매개변수를 특수한 타입과 값으로 제한한, 전체 특수화된 템플릿이기 때문에 단점이 존재합니다.

그것은 데이터가 출력되는 횟수가   3   으로 고정된다는 것입니다.

 

이때 생각할 수 있는 것이 템플릿의 부분 특수화입니다.

부분 특수화( partial specialization )는 전체 템플릿 매개변수를 원하는 타입이나 값으로 제한하는 특수화( full specialization )가 아니라, 일부분의 매개변수만을 제한하는 특수화를 말합니다.

 

그런데,

함수 템플릿의 부분 특수화( partial specialization )는 금지되었습니다.

이 내용은 아마 혼란을 가져올 것입니다.

위의 문장을 읽었을 때, 아마 아래와 같은 코드가 생각났을 것이기 때문입니다.

그리고, 이 코드는 제대로 작동합니다.

template < typename T, int N >  // 기본 함수 템플릿
void print( T data ){
    for( int i = 0; i < N; i++){
        cout << data << " ";
    }
}

// bool 타입의 데이터를 임의의 회수만큼 출력하는 템플릿
template< int N >
void print( bool data){
    for( int i = 0; i < N; i++){
        cout << boolalpha << data << " ";
    }
}

int main(){
    print<int, 3>( 5 );
    print< 4 >( true );	// 4회 출력
}

▼출력

5 5 5 true true true true

그러나, 위에서 두 번째의   print( bool data )   함수 템플릿은, 그 위에 정의되어 있는   print( T data )   템플릿을 부분 특수화한 템플릿이 아니라, 오버로딩( overloading )된 함수 템플릿입니다.

 

위 기본 함수 템플릿을 부분 특수화한 템플릿은 다음과 같아야 합니다.

template < typename T, int N >  // 기본 함수 템플릿
void print( T data ){
    for( int i = 0; i < N; i++){
        cout << data << " ";
    }
}

template< int N >   // 부분 특수화
void print<bool>( bool data){   // error !!
    for( int i = 0; i < N; i++){
        cout << boolalpha << data << " ";
    }
}

위와 같이, 기본 함수 템플릿( primary function template )에서 형식 템플릿 매개변수   T     bool   타입으로 대체해서 만들어진 템플릿이 특수화된 함수 템플릿입니다.

그리고, 이 특수화된 함수 템플릿은 기본 함수 템플릿의 모든 매개변수를 특수한 타입이나 값으로 제한하지 않기 때문에, 부분 특수화된 함수 템플릿임을 알 수 있습니다.

그리고, 이 함수 템플릿은 오류를 발생시킵니다.

 

이렇게, C++을 만들어가는 사람들이 부분 특수화된 함수 템플릿을 만들 수 없도록 금지시킨 이유는, 위의 오버로딩된 함수 템플릿만으로도 얼마든지 기본 함수 템플릿의 기능과 구분이 되는 특별한 기능을 구현할 수 있다고 생각하기 때문입니다.

여러 가지 이유로 부분 특수화를 구현할 수 없기 때문에 금지시킨 것이 아니라는 것입니다.

template < typename T, int N >  // 기본 함수 템플릿
void print( T data ){
    for( int i = 0; i < N; i++){
        cout << data << " ";
    }
}

// bool 타입의 데이터를 임의의 회수만큼 출력하는 오버로딩 템플릿
template< int N >
void print( bool data){
    for( int i = 0; i < N; i++){
        cout << boolalpha << data << " ";
    }
}

그리고, 위에서 링크한 글에서 말했듯이, 이 오버로딩된 템플릿은 기본 함수 템플릿( primary function template ) 보다 더 적은 수의 템플릿 매개변수를 갖게 되므로( 특수한 타입으로 기능을 제한했기 때문에 ), 컴파일러는 이 오버로딩된 템플릿을 먼저 선택합니다.

게다가, 만약 오버로딩된 템플릿으로 특수한 기능을 구현하는 것이 불가능하면, 일반 함수에서 특별한 기능을 구현하도록 처리하면 됩니다.

 

컴파일러는 다음과 같은 순서로 호출할 함수를 선택합니다.

 

일반 함수 > 적은 매개변수를 가진 템플릿에서 생성된 함수 > 템플릿에서 생성된 함수

 

그런데, 함수 템플릿의 전체 특수화( full specialization )는 왜 금지시키지 않은 걸까요?

그것은, 전체 특수화된 템플릿 함수를 대신할 수 있는 오버로딩 템플릿 함수를 정의할 수 없기 때문입니다.

template < typename T, int N >  // 기본 함수 템플릿
void print( T data ){
    for( int i = 0; i < N; i++){
        cout << data << " ";
    }
}

template<>  // 전체 특수화
void print<bool, 3>( bool data){
    for( int i = 0; i < 3; i++){
        cout << boolalpha << data << " ";
    }
}

// 오버로딩된 함수 템플릿??
// 전체 특수화 코드와 비슷하지만, 기본 함수 템플릿이 없으므로 오류.
template<>	
void print( bool data){
    for( int i = 0; i < 3; i++){
        cout << boolalpha << data << " ";
    }
}

그렇기 때문에, 함수 템플릿의 전체 특수화( full specialization )는 합법입니다.

 

 

클래스 템플릿의 부분 특수화

함수와 달리, 클래스는 오버로딩( overloading )이라는 기능이 없기 때문에, 클래스 템플릿을 전체 특수화할 수 있을 뿐 아니라 부분 특수화도 할 수 있습니다.

 

아래의 클래스 템플릿은 임의의 타입의 원소를 임의의 개수만큼 저장하고, 출력할 수 있는 기능을 정의하고 있습니다.

template < typename T, int N >	// 클래스 템플릿
class MyArray{

    T m_array[N];
public:

    T& operator[]( int idx){
        return m_array[idx];
    }

    void print();
};

// 클래스 템플릿의 멤버 함수 정의를 외부에 구현
template < typename T, int N >
void MyArray<T, N>::print(){
    for( int i = 0; i < N; i++){
        cout << m_array[i] << " ";
    }
    cout << endl;
}

이 클래스 템플릿을 사용해서 클래스를 생성하고, 이 클래스의 객체가 가진 데이터를 출력하는 코드는 다음과 같습니다.

int main(){

    constexpr int size = 5;
    MyArray<int, size> arr1;
    for( int i = 0; i < size; i++)
        arr1[i] = i;

    MyArray<bool, size> arr2;
    for( int i = 0; i < size; i++){
        arr2[i] = static_cast<bool>( i % 2);    // 홀수면 true, 짝수면 false
    }

    arr1.print();
    arr2.print();
}

▼출력

0 1 2 3 4 
0 1 0 1 0

그런데 막상 클래스 객체의 데이터를 출력하고 보니 원하는 결과가 아닙니다.

원하는 기능은,   bool   타입의 데이터를 가진 MyArray 클래스 객체의 원소들을 true와 false 형태로 출력되도록 하는 것입니다.

즉, 클래스 템플릿의 특수화를 원하는 것입니다.

하지만, 이전 항목에서와 비슷한 이유로, 클래스 템플릿을 전체 특수화하게 되면, 이 특수화된 템플릿은 임의의 개수의 원소를 가질 수 없습니다. 

// 클래스 멤버 함수 특수화
template<>
void MyArray<bool, 5>::print(){
    for( int i = 0; i < 5; i++){
        cout << boolalpha << m_array[i] << " ";
    }
    cout << endl;
}

▼출력

0 1 2 3 4 
false true false true false

 

이러한 방식의 특수화에 관한 내용은 여기서 볼 수 있습니다.

 

[C++] 클래스 템플릿의 특수화( class template specialization )

클래스 템플릿의 특수화이전 '함수 템플릿의 특수화'에서 얘기했듯이, 템플릿의 특수화는 보편적인 기능을 구현한 템플릿의 정의와는 다른 기능을 수행하기 위한 방법입니다. [C++] 함수 템플릿

codingembers.tistory.com

 

다시 말하면, 위와 같이 전체 특수화( full specialization )를 하면 출력 방식은 원하는 대로 됐지만, 원소의 개수는 항상 5개여야 됩니다.

그래서, 이럴 경우엔 클래스 템플릿의 부분 특수화( partial specialization )를 수행해야 합니다.

즉, 원소의 데이터 타입은   bool   로 제한하고, 원소의 개수는 임의의 수가 되도록 특수화하는 것입니다.

 

이를 구현하려면, 클래스의 멤버 함수인 print만 특별한 기능을 가지면 되므로, 다음과 같이 할 수 있습니다.

template<int N> // 멤버 함수의 부분 특수화
void MyArray<bool, N>::print(){	// error !
    for( int i = 0; i < N; i++){
        cout << boolalpha << m_array[i] << " ";
    }
    cout << endl;
}

그런데, 위의 코드를 컴파일하면 오류를 만나게 됩니다.

멤버 함수 템플릿도 함수 템플릿이고, 함수 템플릿은 부분 특수화를 할 수 없기 때문입니다.

 

이 방식이 안되므로, 컴파일러가 했었으면 하는 일 - 멤버 함수를 특수화하는 코드로부터 클래스 템플릿을 특수화하는 것 -을 직접 수행해야 합니다.

바로, 클래스 템플릿의 부분 특수화를 구현하는 것입니다.

template< int N >  // 부분 특수화 
class MyArray<bool, N>{
     bool m_array[N];
public:

    bool& operator[]( int idx){
        return m_array[idx];
    }

    void print();
};

template< int N >
void MyArray<bool, N>::print(){
    for( int i = 0; i < N; i++){
        cout << boolalpha << m_array[i] << " ";
    }
    cout << endl;
}

이제, 데이터를 출력하면,   bool   타입의 데이터만 특별한 방법으로 출력됩니다.

하지만, 불만이 없는 것은 아닙니다.

위와 같이, 부분 특수화를 구현하기 위해 너무 많은 코드가 복사되었습니다.

이렇게 중복되는 코드는 수가 늘어날수록 골칫거리가 될 가능성이 높습니다.

 

다행히 이러한 문제를 우회하는 방법이 있습니다.

바로, 클래스의 상속 기능을 이용하는 것입니다.

template < typename T, int N >
class MyArray_base{ // 변경되지 않는 기능을 정의한 클래스 템플릿

protected:
    T m_array[N];

public:

    T& operator[]( int idx){
        return m_array[idx];
    }

    void print(){
        for( int i = 0; i < N; i++){
            cout << m_array[i] << " ";
        }
        cout << endl;
    }
};

template < typename T, int N >  // MyArray_base에서 상속받은 클래스 템플릿
class MyArray : public MyArray_base<T, N>{
};

// 부분 특수화한 MyArray_base 클래스 템플릿을 상속받은 클래스 템플릿
template < int N >
class MyArray<bool, N> : public MyArray_base<bool, N>{

public:
    void print(){   // 특별한 기능을 구현한 멤버 함수
        for( int i = 0; i < N; i++){
            cout << boolalpha << this->m_array[i] << " ";
        }
        cout << endl;
    }
};

이 방법은 변경되지 않는 기능을 구현한 코드들을 담고 있는 클래스 템플릿을 부모 클래스 템플릿으로 만드는 것입니다.

그리고, 이 부모 클래스 템플릿을 부분 특수화한 클래스 템플릿   MyArray_base<bool, N>   으로부터 상속받은 클래스 템플릿   MyArray<bool, N>   에서 print 멤버 함수를 오버라이딩( overriding )하는 것입니다.

 

위에서 보았듯이, 중복되는 코드는 MyArray_base 클래스 템플릿에 들어있고, 변경이 필요한 코드만 부분 특수화된 클래스 템플릿에 구현되었습니다.

 

포인터 타입의 부분 특수화

위에서 링크한 ' 클래스 템플릿의 특수화( class template specialization )'에서 다음과 같은 클래스 템플릿을 작성했었습니다.

이 템플릿의 기능은 임의의 타입의 데이터를 저장하고 화면에 출력하는 것입니다.

#include <iostream>
using namespace std;

template < typename T > // 클래스 템플릿
class DataPrinter{

    T m_Data;
public:
    DataPrinter(T data) : m_Data{data}{};

    void print(){
        cout << m_Data << endl;
    }
};

int main(){

    DataPrinter intData{ 5 };
    intData.print();
}

▼출력

5

그런데, 이 클래스 템플릿에 포인터 타입의 데이터를 대입하면 다음과 같이 출력됩니다.

int main(){

    double dData{ 3.14 };
    double* pdData{ &dData };

    DataPrinter doublePtr{ pdData };	// 포인터 타입의 데이터로 초기화
    doublePtr.print();
}

▼출력

double data: 0x5ffe48

이렇게 되는 이유는, 컴파일러가 기본 템플릿( primary template )으로부터 다음과 같은 구체화( instantiation )된 함수를 생성하기 때문입니다.

template<>
class DataPrinter<double*>{ // 암시적인 구체화 ( 실제로 보이지 않음 )
    double* m_Data;
public:
    DataPrinter( double* data) : m_Data{data}{};

    void print(){	
        cout << "double data: " << m_Data << endl;
    }
};

이때의   m_Data   의 타입은   double*   타입이므로, 출력의 결과는   double   데이터를 담고 있는 변수의 주소가 되는 것입니다.

 

그럼, 포인터 타입을 입력하더라도, 포인터가 가리키고 있는 대상의 값을 출력하려면 어떻게 해야 할까요?

클래스 템플릿을 전체 특수화( full specialization )는 방법은 템플릿 인수가   double*   타입일 때는 원하는 값을 얻을 수 있지만, 다른 포인터 타입의 경우는 똑같은 결과( 대상의 주소가 출력되는 현상 )를 가져올 것입니다.

즉, 이러한 접근은 근본적인 해결이 안 된다는 것입니다.

 

이때, 사용할 수 있는 것이 클래스 템플릿의 부분 특수화( partial specialization )입니다.

 

template< typename T >	// 부분 특수화
class DataPrinter<T*>{
    T* m_Data;
public:
    DataPrinter( T* data) : m_Data( data){};

    void print(){   // 포인터가 가리키는 값을 출력
        cout << "pointer data: " << *m_Data << endl;
    }
};

'이것이 어떻게 부분 특수화인가' 하고 약간의 혼동이 올 수 있는데, 사실 템플릿의 특수화는 어떤 타입( 또는 비-형식 템플릿을 대체하는 경우엔 어떤 값 )을 사용하도록 제한하는 것입니다.

위의 클래스 템플릿도 형식 템플릿 매개변수( type template parameter )   T   가 쓰여야 할 곳에   T*   타입을 사용하도록 제한하고 있습니다.

그리고, 템플릿 매개변수 선언( template parameter declaration )에는 아직 템플릿 매개변수가 남아 있습니다.

모든 매개변수가 특정한 타입으로 대체되면 전체 특수화, 그렇지 않으면 부분 특수화라고 한 것을 기억하고 있을 것입니다.

위 예문은 기본 클래스 템플릿의 부분 특수화가 맞습니다.

 

이제, DataPrinter 클래스 템플릿에 어떠한 포인터 타입을 대입하더라도, 위의 부분 특수화된 클래스 템플릿이 사용되고, 원하는 대로, 포인터가 가리키는 객체를 출력하게 될 것입니다.

 

전체 코드는 다음과 같습니다.

#include <iostream>
using namespace std;

// 기본 클래스 템플릿 ( 포인터 타입이 아닐 때 사용 )
template < typename T > 
class DataPrinter{

    T m_Data;
public:
    DataPrinter(T data) : m_Data{data}{};

    void print(){
        cout << m_Data << endl;
    }
};

// 부분 특수화한 클래스 템플릿 ( 포인터 타입일 때 사용 )
template< typename T >
class DataPrinter<T*>{
    T* m_Data;
public:
    DataPrinter( T* data) : m_Data( data){};

    void print(){   // 역참조 연산자 사용
        cout << "pointer data: " << *m_Data << endl;
    }
};

int main(){

    DataPrinter intData{ 5 };
    
    double dData{ 3.14 };
    DataPrinter doubleData{ dData };

    double* pdData{ &dData };
    DataPrinter ptrData{ pdData };	// 포인터 타입 데이터 출력
    
    intData.print();
    doubleData.print();
    ptrData.print();
}

 

 

 

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