본문 바로가기

진리는어디에

[C++] 템플릿 특화(Template specialize) 완벽 가이드

템플릿 특화란?

C++은 하나의 코드로 다양한 타입에 대응할 수 있는 템플릿이라는 도구를 제공한다. 이 글을 읽는 여러분은 이미 템플릿에 대해서는 알고 있다고 가정하고, 이번 포스트에서는 템플릿 특화(Template specialization)에 대해 살펴 보도록하낟.

템플릿 특화(Template specialization)는 특정 타입의 템플릿 인자에 대응하는 특별한 템플릿 클래스를 정의할 수 있는 기능이다. 참고로 템플릿 특화는 템플릿 특수화라고도 번역 되며 특화나 특수화나 같은 의미니 다른 곳에서 특수화라고 표현되는 글을 본다고 하더라도 당황하지 말도록 한다.

설명을 위해 우선 아래 일반 템플릿 클래스의 예제 코드를 살펴 보도록 하자. 아래에서는 템플릿 인자로 T와 U를 받을 수 있는 Widget이라는 템플릿 클래스를 정의하고 있다. 아래와 같이 정의된 Widget 템플릿 클래스는 템플릿 인자로 int 같은 primitive 타입 부터 복잡한 구조를 가진 클래스 같은 다양한 타입을 인자로 넘겨 받을 수 있다.

// 일반적인 템플릿 클래스의 선언과 사용

template <class T, class U>
class Widget 
{
    // ...
};

Widget<int, double>    w1; // 간단한 인자로 선언된 Widget 템플릿 클래스

class Window
{
    // ...
};

Widget<Window, double> w3; // 복잡한 클래스 인자로 선언된 Widget 템플릿 클래스

위 예제와 비교하며 다음 예제를 살펴 보자. 16 라인을 주목해 보면 일반 템플릿 클래스와 구분되는 특이한 포멧을 가진 Widget 클래스가 정의되어 있는것을 확인할 수 있다.

class ModalDialog
{
};

class MyController
{
};

template <class T, class U>
class Widget 
{
    ...일반화된 구현...
};

template <>
class Widget <ModalDialog, MyController> 
{
    ...ModalDialog, MyController 에만 특화된 구현...
};

이 특이한 포멧의 Widget 클래스는 템플릿 인자 T, U 대신, 클래스 이름 뒤에 ModalDialog, MyController와 같은 특정한 타입을 미리 지정하여 정의 되었다. 이 처럼 템플릿 인자에 미리 특정 타입을 정의한 템플릿 클래스를 템플릿 특화 클래스(specialized template class)라고 하며, 이런 특화된 클래스는 'Widget<ModalDialog, MyController>' 처럼 미리 정의된 ModalDialog와 MyController를 템플릿 인자로 받는 클래스를 선언하는 경우 해당 타입에 특화된 클래스를 생성한다.

반면,10 라인의 기본 템플릿 클래스(primary template class)는 Widget<BaseWindow, BaseController>과 같이 미리 지정되지 않은 타입을 이용해 Widget 클래스를 선언하는 경우 사용된다.

이렇게 특정 데이터 타입에만 특화된 구현을 사용할 수 있도록 하는 것을 템플릿 특화라고 하며, 다음 섹션에서는 보다 자세한 예제를 이용하여 템플릿 특화에 대해서 보다 깊이 살펴 보도록 하자.

템플릿 특화를 이용한 최적화 예제

이전 섹션에서는 템플릿 특화의 기본 개념과 특화된 클래스의 포멧에 대해 살펴 보았다. 이번 섹션에서는 템플릿 어떤 경우에 사용되는지 Vector라는 템플릿 클래스를 함께 만들어 가며 살펴 보도록하겠다. 먼저 아래 Vector 템플릿 클래스는 템플릿 인자로 넘어온 타입의 배열을 내부적으로 생성/관리하는 클래스다.

template <class T>
class Vector
{
    T* ptr;
public :
    Vector(std::size_t size)
    {
        ptr = new T[size];
    }

    ~Vector()
    {
        delete[] ptr;
    }
};

위 Vector 클래스는 아래와 같이 다양한 템플릿 인자 타입에 대응하는 배열을 가진 클래스를 생성할 수 있다.

int main()
{
    Vector<int>     v1(5); // 사이즈가 5인 int 타입 배열(20 바이트)
    Vector<double>  v2(5); // 사이즈가 5인 double 타입 배열(40 바이트)
    Vector<bool>    v3(5);// 사이즈가 5인 double 타입 배열(5 바이트)
    
    return 0;
}

여기까지는 우리가 일반적으로 알고 있는 템플릿 클래스의 사용 방법이다. 하지만 위 5라인의 Vector<bool>을 주목 해보자. C++에서 bool 타입은 1바이트의 크기를 가지고 있으며, 위 Vector<bool>의 선언은 총 5바이트의 크기를 가지게 된다. 하지만 bool은 참 또는 거짓만을 나타낼수 있으면 충분하므로 굳이 1바이트 저장 공간 보다는 0 또는 1만을 저장할 수 있는 bit를 타입으로 사용하는 것이 메모리 절약 측면에서 훨씬 이득이다. 위 예제에서는 배열의 크기가 5이기 때문에 큰 차이가 발생하지 않지만 500, 5000과 같이 배열의 크기가 커지면 커질수록 그 차이는 벌어진다.

이런 경우 이전 섹션에서 살짝 살펴 보았던 '템플릿 특화'를 이용하여 임의의 타입인 경우에는 기본 Vector 클래스를 사용하고 bool 타입에 대해서만 사용되는 특수한 Vector 클래스를 정의 할 수 있다.

// primary template
template <class T>
class Vector
{
    T* ptr;
public :
    Vector(std::size_t size)
    {
        ptr = new T[size];
    }

    ~Vector()
    {
        delete[] ptr;
    }
};

// specialized template
template <>                           // 템플릿 타입이 지정되었으므로 필요 없다
class Vector<bool>                    // 특정 타입에 대응하겠다는 의미
{
    bool* ptr;                        // 템플릿 인자 T가 없으므로 더 이상 T 사용 불가
public :
    Vector(std::size_t size)
    {
        ptr = new bool[size / 8 + 1]; // 바이트를 bit 단위로 계산
    }

    ~Vector()
    {
        delete[] ptr;
    }
};

특수한 타입에 대응하는 템플릿 클래스는 20라인에서와 같이 클래스 이름 뒤에 템플릿 인자 타입을 명시하여 정의한다. 이미 템플릿 인자가 지정되어 있으므로 19라인의 템플릿 인자 리스트는 더 이상 필요 없다. 원본 클래스에 템플릿 인자 리스트가 있더라도 특화된 클래스에서는 필요 없다면 19라인 처럼 리스트를 비워둘 수 있다. 이에 대한 자세한 설명은 다음 섹션에서 '부분 특화'를 살펴 보면서 다시 다루어 보도록 하겠다.

26 라인의 Vector 클래스의 생성자에서는 bool 타입 배열을 주어진 사이즈로 생성하는 것이 아니라 bit 크기로 나누어 실제 필요한 만큼의 bit를 가지는 bool 타입 배열을 만들어 필요한 메모리를 절약하고 있다.

이렇게 템플릿 클래스를 만들고, 특정한 타입일 때 다른 구현을 사용할 수 있는 '템플릿 특화' 기능을 이용하여 프로그램을 최적화 시킬수 있다.

템플릿 부분 특화(Partial Specialization)

이젠 섹션에서 우리는 템플릿 특화의 기본 문법과 사용 방식에 대해 살펴 보았다. 이번 섹션에서는 특정 템플릿 인자만을 명시적으로 지정하고 나머지 템플릿 인자들에 대해서 일반화 하는 부분 템플릿 특화(Partial Template Specializtion)에 대해 예제를 들어 보며 살펴 보도록 하겠다. 

먼저 아래 예제를 살펴 보자. Object라는 템플릿 클래스가 정의 되어 있고 아래는 다양한 템플릿 인자를 이용해 Object 클래스의 fn 스태틱 함수를 호출하고 있다.

#include <iostream>
template <class T, class U>
struct Object
{
    static void fn() 
    {
        std::cout << "T, U" << std::endl;
    }
};

int main()
{
    Object<char,   double>::fn();               // T, U
    Object<int,    short>::fn();                // T, U
    Object<short*, double>::fn();               // T, U
    Object<float,  float>::fn();                // T, U
    Object<int,    float>::fn();                // T, U
    Object<int,    Object<char, short>>::fn();  // T, U

    return 0;
}

위 상태에서는 모든 fn 스태틱 함수는 'T, U'라는 결과를 출력한다. 그럼 이제 부터 사용자가 템플릿 인자를 어떻게 사용하느냐에 따라 fn함수가 다른 결과를 출력하도록 하는 방법에 대해 살펴 보도록 하자.

특정 타입에 대응하는 특화

template <>
struct Object<int, short>
{
    static void fn() 
    {
        std::cout << "int, short" << std::endl;
    }
};

Object<int,    short>::fn();                // int, short

위 예제에서는 int와 short 타입의 인자에 대해 특화를 적용했다. 모든 템플릿 인자들이 이미 지정되어 있으므로 클래스 이름 앞에 정의 되는 템플릿 인자 리스트는 필요 없으므로 비워 둔다. 위 클래스의 fn 스태틱 함수는 'T, U' 대신 'int, short'를 출력한다.

포인터 타입에 대응하는 특화

아래 예제는 임의의 타입 T가 포인터 타입인 경우 특수화된 클래스를 사용하겠다는 의미다. 템플릿 특화를 사용하지만 'class T, class U'와 같은 템플릿 인자 리스트가 존재하고 있다는 부분에 주목하자. 템플릿 특화에서 템플릿 인자 리스트는 무조건 비우는 것이 아니라 필요에 따라 정의될 수도 있고 그렇지 않을 수도 있다. 뒤에 소개 되겠지만 심지어 더 많을 수도 있다는것을 유의하자.

template <class T, class U>
struct Object<T*, U>
{
    static void fn()
    {
        std::cout << "T*, U" << std::endl;
    }
};

Object<short*, double>::fn();               // T*, U

같은 타입을 사용하는 특화

template <class T>
struct Object<T, T>
{
    static void fn()
    {
        std::cout << "T, T" << std::endl;
    }
};

Object<float,  float>::fn();                // T, T

위 예제는 같은 타입을 이용하여 Object 템플릿 클래스를 선언하고 있다. 이번에는 타입이 한 종류 밖에 없으므로 템플릿 인자 리스트에는 'class T' 하나만 정의 되어 있다. 프라이머리 템플릿 클래스에서는 템플릿 인자 두 개가 정의 되어 있지만 이번 경우에는 하나 밖에 없다.

다시 한번 말하지만 템플릿 부분 특화에서 템플릿 인자 리스트는 필요에 따라 다양하게 정의될 수 있다. 하지만 클래스 이름 뒤에 정의 되는 특화 타입은 템플릿 원형의 포멧을 따라야만 한다는 것을 꼭 명심하도록 하자.

템플릿 인자 리스트의 일부만 지정하는 특화

template <class U>
struct Object<int, U>
{
    static void fn()
    {
        std::cout << "int, U" << std::endl;
    }
};

Object<int,    float>::fn();                // int, U

위 예제는 첫 번째 템플릿 인자만을 명시적으로 지정하고 두 번째 인자는 모든 타입에 대해 일반화하고 있다. 이런 경우 첫번째 템플릿 인자가 int인 경우 부분 특화된 구현을 사용하게 된다.

템플릿 인자 리스트 보다 많은 인자에 대한 특화

이전 섹션들에서는 템플릿 인자 리스트 보다 적거나 같은 수의 인자들에 대해 특화를 수행하는 예들을 보였다. 하지만 프리미티브 템플릿 클래스 보다 많은 수의 템플릿 인자를 갖는 복잡한 인자에 대한 특화도 가능하다. 아래는 복잡한 템플릿 인자에 대해 특화를 진행함으로써 프리미티브 템플릿 인자 보다 많은 인자를 정의하는 예제를 보여주고 있다.

template <class A, class B, class C> // primitive template class 보다 인자가 많음
struct Object<A, Object<B, C>> // 여기는 primitive template class와 같음
{
    static void fn()
    {
        std::cout << "A, Object<B, C>" << std::endl;
    }
};

Object<long,   Object<char, short>>::fn();  // A, Object<B, C>
Object<int,    Object<char, short>>::fn();  // ERROR!!

위 예제에서는 두 번째 인자로 복잡한 구조의 템플릿 인자(Object<B, C>)를 받으므로써 총 세 개의 템플릿 인자가 필요하게 되어 템플릿 인자 리스트에는 세 개의 인자가 정의되게 된다. 이렇게 템플릿 특화 클래스는 필요에 따라 원본 보다 많거나 적은 템플릿 인자 리스트를 가질 수 있다는 것에 유의하자.

또한 11 라인에서는 첫 번째 인자가 int로 시작하는 Object 템플릿 클래스 예제를 보여주고 있는데, 이 경우엔 이전 섹션에 정의 했던 int로 시작하는 템플릿 특화와 조건상 충돌을 일으켜 컴파일 에러를 발생 시킨다. 이를 해결하기 위해서는 아래와 같이 int로 시작하며 Object<B, C>를 두 번째 인자로 받는 특화 클래스를 별도 추가 해주어야 한다.

template <class B, class C> 
struct Object<int, Object<B, C>>
{
    static void fn()
    {
        std::cout << "int, Object<B, C>" << std::endl;
    }
};

 

부분 특화 제약 사항

템플릿 파라메터의 부분 특화는 클래스에서만 적용 할 수 있고, 클래스의 템플릿 멤버 함수 또는 일반 템플릿 함수와 같이 함수에 대해서는 적용이 불가능하다. 단 일반 템플릿 특화는 함수를 대상으로도 가능하다.

마치며

이상 템플릿 특화와 부분 특화에 대해 살펴 보았다. C++에서 템플릿 특화는 클래스 또는 멤버 함수, 일반 함수에 모두 적용할 수 있지만, 부분 특화는 클래스에 대해서만 적용 가능하다. 그리고 부분 특화시 필요에 따라 템플릿 인자는 원본 보다 많거나 적을 수 있지만 인자를 사용하는 부분(클래스 이름 뒤의 템플릿 인자)은 원본과 같아야만 한다는 것을 명심 하도록 하자.

1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!