icon
11장 : 템플릿 프로그래밍

템플릿 특수화

템플릿은 대부분의 경우 매우 유용하지만, 때로는 특정 타입에 대해서는 템플릿의 일반적인 구현이 적절하지 않거나, 더 효율적인 특별한 동작을 제공하고 싶은 경우가 발생합니다.

이럴 때 사용하는 것이 바로 템플릿 특수화(Template Specialization) 입니다.

이번 장에서는 템플릿 특수화의 개념과 사용법, 그리고 부분 특수화(Partial Specialization)명시적 특수화(Explicit Specialization 또는 Full Specialization) 의 차이점에 대해 자세히 알아보겠습니다.


템플릿 특수화의 필요성

템플릿은 모든 타입에 대해 "일반적인" 방식으로 동작하도록 설계됩니다.

하지만 특정 타입의 특성 때문에 일반적인 템플릿 구현이 문제가 되거나, 혹은 더 나은 성능을 낼 수 있는 특별한 방법이 있을 수 있습니다.

예시

  • std::string과 같은 문자열 타입은 일반적인 char 배열과 다르게 동작합니다. std::string의 복사는 문자열 전체를 복사하는 깊은 복사를 의미할 수 있습니다.
  • bool 타입은 메모리 효율성을 위해 특별하게 처리되는 경우가 많습니다. (예: std::vector<bool>)
  • char* (C-스타일 문자열)와 같은 포인터 타입은 일반적인 포인터와는 다른 비교나 복사 의미를 가질 수 있습니다.

이러한 경우 특정 타입에 대해서만 템플릿의 동작을 재정의하는 것이 필요하며, 이것이 템플릿 특수화의 목적입니다.


함수 템플릿의 명시적 특수화

함수 템플릿은 특정 타입에 대해 완전히 다른 구현을 제공할 수 있습니다. 이를 함수 템플릿의 명시적 특수화라고 합니다.

함수 템플릿 특수화 형식
template <> // 템플릿 매개변수 리스트는 비워둡니다.
반환타입 함수이름<특수화할_타입>(매개변수) {
    // 특수화된 타입에 대한 특별한 구현
}

std::swap은 이미 모든 타입에 대해 잘 동작하는 템플릿이지만, 특정 타입에 대한 특수화를 어떻게 하는지 보여주기 위한 예시입니다.

예를 들어 const char* (C-스타일 문자열)에 대한 swap은 포인터 값만 교환하는 것이 아니라, 실제 문자열 내용을 교환해야 할 수도 있습니다.

swap 함수 템플릿 특수화 (과거의 예시, 현재는 스마트 스왑이 더 좋음)
#include <iostream>
#include <string>
#include <cstring> // C-스타일 문자열 함수를 위해

// 1. 일반 함수 템플릿 정의
template <typename T>
void mySwap(T& a, T& b) {
    std::cout << "일반 템플릿 mySwap() 호출: ";
    T temp = a;
    a = b;
    b = temp;
}

// 2. int 타입에 대한 명시적 특수화
// template <> void mySwap<int>(int& a, int& b) {
//     std::cout << "int 명시적 특수화 mySwap() 호출: ";
//     int temp = a;
//     a = b;
//     b = temp;
// }
// 일반적으로 기본 타입에 대한 특수화는 잘 하지 않습니다. 
// 성능상 이득이 없거나 더 나빠질 수 있기 때문입니다.

// 3. const char* 타입에 대한 명시적 특수화
// 포인터 값만 교환하는 것이 아니라, 실제 문자열 내용을 교환하도록 구현
template <>
void mySwap<const char*>(const char*& a, const char*& b) {
    std::cout << "const char* 명시적 특수화 mySwap() 호출 (내용 교환): ";
    // char 배열을 동적으로 할당하여 내용 복사
    char* temp_a = new char[strlen(a) + 1];
    strcpy(temp_a, a);

    char* temp_b = new char[strlen(b) + 1];
    strcpy(temp_b, b);
    
    // 포인터 교환 (원래 a가 가리키던 곳에 b의 내용, b가 가리키던 곳에 a의 내용)
    delete[] a;
    a = temp_b;

    delete[] b;
    b = temp_a;
}

int main() {
    int i1 = 10, i2 = 20;
    mySwap(i1, i2); // int 일반 템플릿 (또는 int 특수화) 호출
    std::cout << "i1: " << i1 << ", i2: " << i2 << std::endl; // 출력: i1: 20, i2: 10

    double d1 = 1.1, d2 = 2.2;
    mySwap(d1, d2); // double 일반 템플릿 호출
    std::cout << "d1: " << d1 << ", d2: " << d2 << std::endl; // 출력: d1: 2.2, d2: 1.1

    const char* s1 = "Hello";
    const char* s2 = "World";
    mySwap(s1, s2); // const char* 명시적 특수화 호출
    // s1과 s2가 가리키는 문자열 내용이 바뀜 (주의: 동적 메모리 할당 및 해제)
    std::cout << "s1: " << s1 << ", s2: " << s2 << std::endl; // 출력: s1: World, s2: Hello

    // Note: C-스타일 문자열을 사용하는 mySwap 특수화는 실제로는 메모리 누수 위험이 있고,
    // std::string을 사용하거나 스마트 포인터를 사용하는 것이 더 안전합니다.
    // 이 예시는 특수화의 개념을 보여주기 위함입니다.

    std::string str1 = "Apple";
    std::string str2 = "Banana";
    mySwap(str1, str2); // std::string 일반 템플릿 호출 (std::string 자체는 깊은 복사 지원)
    std::cout << "str1: " << str1 << ", str2: " << str2 << std::endl; // 출력: str1: Banana, str2: Apple

    return 0;
}

함수 템플릿 특수화의 주의사항

  • 함수 템플릿 특수화는 오버로딩과 다릅니다. 컴파일러는 특수화된 버전을 "더 적합한" 함수로 간주하여 선택합니다.
  • 일반적으로 함수 템플릿 특수화보다는 함수 오버로딩을 선호합니다. 특정 타입에 대한 동작이 너무 달라 템플릿의 원래 의도에서 벗어나는 경우에만 특수화를 고려합니다.
  • C++11 이후에는 함수 템플릿 특수화 대신 std::enable_ifif constexpr 같은 SFINAE(Substitution Failure Is Not An Error) 기법을 사용하여 템플릿 인스턴스화를 제어하는 것이 더 유연하고 권장됩니다.

클래스 템플릿의 명시적 특수화

클래스 템플릿도 특정 타입에 대해 완전히 다른 구현을 제공할 수 있습니다.

이를 클래스 템플릿의 명시적 특수화라고 합니다.

클래스 템플릿 특수화 형식
template <>
class 클래스이름<특수화할_타입> {
    // 특수화된 타입에 대한 특별한 멤버 변수 및 멤버 함수 구현
};

일반적으로 MyValue 템플릿은 어떤 타입이든 저장하지만, bool 타입에 대해서는 메모리 효율성을 위해 특별한 방식으로 처리한다고 가정해 봅시다.(실제 std::vector<bool>이 이런 식으로 특수화되어 있습니다.)

MyValue 클래스 템플릿 특수화 예시
#include <iostream>
#include <string>

// 1. 일반 클래스 템플릿 정의
template <typename T>
class MyValue {
private:
    T value;
public:
    MyValue(T val) : value(val) {
        std::cout << "일반 MyValue<" << typeid(T).name() << "> 객체 생성: " << value << std::endl;
    }
    T getValue() const { return value; }
    void setValue(T val) { this->value = val; }
    void display() const {
        std::cout << "일반 MyValue 값: " << value << std::endl;
    }
};

// 2. bool 타입에 대한 명시적 특수화
template <>
class MyValue<bool> {
private:
    // bool은 1비트만 필요하므로, 메모리 효율을 위해 다른 방식으로 저장할 수 있음
    // 여기서는 간단히 int로 저장하여 0/1로 표현
    int internalValue;
public:
    MyValue(bool val) : internalValue(val ? 1 : 0) {
        std::cout << "bool 명시적 특수화 MyValue<bool> 객체 생성: " << (val ? "true" : "false") << std::endl;
    }
    bool getValue() const { return (internalValue == 1); }
    void setValue(bool val) { this->internalValue = (val ? 1 : 0); }
    void display() const {
        std::cout << "bool 특수화 MyValue 값: " << (getValue() ? "True" : "False") << std::endl;
    }
};

// 3. char* 타입에 대한 명시적 특수화 (C-스타일 문자열)
template <>
class MyValue<const char*> {
private:
    char* str; // 동적 할당하여 깊은 복사
public:
    MyValue(const char* s) {
        std::cout << "const char* 명시적 특수화 MyValue<const char*> 객체 생성.\n";
        if (s) {
            str = new char[strlen(s) + 1];
            strcpy(str, s);
        } else {
            str = nullptr;
        }
    }
    ~MyValue() {
        std::cout << "const char* 명시적 특수화 MyValue<const char*> 소멸.\n";
        delete[] str;
    }
    // 복사 생성자와 복사 대입 연산자 (규칙 3/5)도 정의해야 하지만, 예시를 위해 생략
    
    const char* getValue() const { return str; }
    void setValue(const char* s) {
        delete[] str;
        if (s) {
            str = new char[strlen(s) + 1];
            strcpy(str, s);
        } else {
            str = nullptr;
        }
    }
    void display() const {
        std::cout << "const char* 특수화 MyValue 값: " << (str ? str : "nullptr") << std::endl;
    }
};


int main() {
    MyValue<int> intVal(123);           // 일반 템플릿 호출
    intVal.display();

    MyValue<double> doubleVal(45.67);   // 일반 템플릿 호출
    doubleVal.display();

    MyValue<bool> boolVal(true);        // bool 특수화 호출
    boolVal.display();
    boolVal.setValue(false);
    boolVal.display();

    MyValue<const char*> charPtrVal("Hello C++"); // const char* 특수화 호출
    charPtrVal.display();
    charPtrVal.setValue("World");
    charPtrVal.display(); // 출력: const char* 특수화 MyValue 값: World

    return 0;
}

위 예시에서 MyValue<bool>MyValue<const char*>은 일반 템플릿 MyValue<T>와는 완전히 다른 방식으로 데이터를 저장하고 처리합니다.

bool은 내부적으로 int로 표현되었고, const char*는 동적 메모리 할당을 통해 깊은 복사를 수행합니다.


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

클래스 템플릿은 모든 템플릿 매개변수를 특수화하지 않고, 일부만 특수화하는 것도 가능합니다.

이를 부분 특수화(Partial Specialization) 라고 합니다.

함수 템플릿은 부분 특수화를 지원하지 않습니다.

클래스 템플릿 부분 특수화 형식
template <typename T> // 하나의 매개변수 T만 남겨둠
class 클래스이름<T, 다른_타입> { // 두 번째 매개변수를 특정 타입으로 고정
    // T 타입에 대한 부분 특수화된 구현
};

template <typename T>
class 클래스이름<T*> { // T 타입 포인터에 대한 부분 특수화
    // T* 타입에 대한 부분 특수화된 구현
};

Pair<T1, T2> 템플릿에서 두 번째 타입이 int인 경우와 포인터인 경우에 대해 부분 특수화를 해봅시다.

Pair 클래스 템플릿 부분 특수화 예시
#include <iostream>
#include <string>

// 1. 일반 클래스 템플릿 정의 (Pair.h에서 가져옴)
template <typename T1, typename T2>
class Pair {
private:
    T1 first;
    T2 second;
public:
    Pair(T1 f, T2 s) : first(f), second(s) {
        std::cout << "일반 Pair<" << typeid(T1).name() << ", " << typeid(T2).name() << "> 객체 생성.\n";
    }
    void display() const {
        std::cout << "일반 Pair: First=" << first << ", Second=" << second << std::endl;
    }
};

// 2. Pair<T, int> 에 대한 부분 특수화 (두 번째 타입이 int인 경우)
template <typename T>
class Pair<T, int> { // 두 번째 템플릿 매개변수를 int로 고정
private:
    T first;
    int second; // int 타입
public:
    Pair(T f, int s) : first(f), second(s) {
        std::cout << "부분 특수화 Pair<" << typeid(T).name() << ", int> 객체 생성.\n";
    }
    void display() const {
        std::cout << "Pair<T, int>: First=" << first << ", Second=" << second << " (int 특수화)\n";
    }
};

// 3. Pair<T, T*> 에 대한 부분 특수화 (두 번째 타입이 첫 번째 타입의 포인터인 경우)
template <typename T>
class Pair<T, T*> { // 두 번째 템플릿 매개변수를 T* (T의 포인터)로 고정
private:
    T value;
    T* ptr;
public:
    Pair(T v, T* p) : value(v), ptr(p) {
        std::cout << "부분 특수화 Pair<" << typeid(T).name() << ", " << typeid(T*).name() << "> (포인터) 객체 생성.\n";
    }
    void display() const {
        std::cout << "Pair<T, T*>: Value=" << value << ", Ptr=" << (ptr ? *ptr : 0) << " (포인터 특수화)\n";
    }
};


int main() {
    Pair<double, char> p1(3.14, 'A');  // 일반 Pair 템플릿 사용
    p1.display(); // 출력: 일반 Pair: First=3.14, Second=A

    Pair<std::string, int> p2("Test", 123); // Pair<T, int> 부분 특수화 사용
    p2.display(); // 출력: Pair<T, int>: First=Test, Second=123 (int 특수화)

    int val = 100;
    Pair<int, int*> p3(50, &val); // Pair<T, T*> 부분 특수화 사용
    p3.display(); // 출력: Pair<T, T*>: Value=50, Ptr=100 (포인터 특수화)
    
    return 0;
}

컴파일러는 객체 생성 시점에 사용 가능한 템플릿 중에서 가장 특수화된 버전을 선택합니다.

일반 템플릿 -> 부분 특수화 템플릿 -> 명시적 특수화 템플릿 순으로 우선순위가 높다고 생각할 수 있습니다.


템플릿 특수화 사용 시 고려사항

  • 남용 금지: 템플릿 특수화는 강력한 기능이지만, 너무 많이 사용하면 코드의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다. 일반적인 템플릿으로 해결할 수 있다면 특수화보다는 일반적인 구현을 선호하는 것이 좋습니다.
  • 오버로딩 vs 특수화: 함수 템플릿의 경우, 특정 타입에 대해 다른 동작을 제공할 때 함수 오버로딩과 특수화를 모두 사용할 수 있습니다. 일반적으로 함수 오버로딩이 더 선호됩니다. 특수화는 템플릿의 기본 동작을 "대체"하는 것이고, 오버로딩은 단순히 "추가적인 함수"를 제공하는 것으로 이해할 수 있습니다.
  • 헤더 파일에 정의: 템플릿 특수화도 일반 템플릿과 마찬가지로, 정의를 .cpp 파일로 분리하면 링커 오류가 발생할 수 있습니다. 따라서 특수화된 템플릿의 정의도 모두 헤더 파일에 포함하는 것이 일반적입니다.