C++ 템플릿 특수화 | template specialization 가이드

C++ 템플릿 특수화 | template specialization 가이드

이 글의 핵심

C++ 템플릿 특수화에 대한 실전 가이드입니다. template specialization 가이드 등을 예제와 함께 상세히 설명합니다.

들어가며

템플릿 특수화(Template Specialization)는 특정 타입이나 타입 패턴에 대해 제네릭 템플릿과 다른 구현을 제공하는 기능입니다. 타입별 최적화, 예외 동작, 트레이트 구현 등에 활용됩니다.


1. 전문화 (Full Specialization)

함수 템플릿 전문화

#include <iostream>
#include <string>

// 일반 템플릿: 모든 타입 T에 대한 기본 구현
template<typename T>
void print(T v) {
    // 기본 동작: 값을 그대로 출력
    std::cout << v << std::endl;
}

// const char* 전문화 (Full Specialization)
// template<>: 특정 타입에 대한 완전히 다른 구현
// 문자열 리터럴에 대해서는 따옴표를 추가
template<>
void print<const char*>(const char* v) {
    std::cout << '"' << v << '"' << std::endl;
}

// std::string 전문화
// string 타입에 대해서도 따옴표 추가
template<>
void print<std::string>(std::string v) {
    std::cout << '"' << v << '"' << std::endl;
}

int main() {
    print(42);           // 일반 템플릿 사용 → 42
    print(3.14);         // 일반 템플릿 사용 → 3.14
    print("hello");      // const char* 전문화 사용 → "hello"
    print(std::string("world"));  // string 전문화 사용 → "world"
    
    // 컴파일러가 가장 구체적인 버전 선택
    
    return 0;
}

선택 과정:

  1. print(42): T = int, 일반 템플릿
  2. print("hello"): T = const char*, 전문화 버전 우선

클래스 템플릿 전문화

#include <iostream>
#include <string>

// 일반 템플릿
template<typename T>
class Storage {
    T data;
public:
    Storage(T d) : data(d) {}
    void print() {
        std::cout << "Generic: " << data << std::endl;
    }
};

// bool 전문화 (비트 최적화)
template<>
class Storage<bool> {
    bool data;
public:
    Storage(bool d) : data(d) {}
    void print() {
        std::cout << "Bool: " << (data ? "true" : "false") << std::endl;
    }
    bool get() const { return data; }
};

int main() {
    Storage<int> s1(42);
    s1.print();  // Generic: 42
    
    Storage<bool> s2(true);
    s2.print();  // Bool: true
    
    return 0;
}

2. 부분 특수화 (Partial Specialization)

포인터 부분 특수화

#include <iostream>

// 일반 템플릿: 모든 타입 T에 대한 기본 구현
template<typename T>
class Wrapper {
    T value;  // 값으로 저장
public:
    Wrapper(T v) : value(v) {}
    void print() {
        std::cout << "Value: " << value << std::endl;
    }
};

// 포인터 부분 특수화 (Partial Specialization)
// template<typename T>: T는 여전히 템플릿 매개변수
// Wrapper<T*>: T*에 대한 특수화 (모든 포인터 타입)
// 포인터 타입에 대해서는 다른 동작 제공
template<typename T>
class Wrapper<T*> {
    T* ptr;  // 포인터로 저장
public:
    Wrapper(T* p) : ptr(p) {}
    void print() {
        // 포인터 특수화: nullptr 체크 후 역참조
        if (ptr) {
            // *ptr: 포인터가 가리키는 값
            // ptr: 포인터 주소
            std::cout << "Pointer: *" << *ptr << " (at " << ptr << ")" << std::endl;
        } else {
            std::cout << "Pointer: nullptr" << std::endl;
        }
    }
};

int main() {
    int x = 100;
    
    // 일반 템플릿 사용: T=int
    Wrapper<int> w1(42);
    w1.print();  // Value: 42
    
    // 포인터 부분 특수화 사용: T=int, Wrapper<int*>
    Wrapper<int*> w2(&x);
    w2.print();  // Pointer: *100 (at 0x...)
    
    // nullptr도 안전하게 처리
    Wrapper<int*> w3(nullptr);
    w3.print();  // Pointer: nullptr
    
    return 0;
}

배열 부분 특수화

#include <iostream>

// 일반 템플릿
template<typename T>
struct ArrayTraits {
    static constexpr bool is_array = false;
    static constexpr size_t size = 0;
};

// 배열 부분 특수화
template<typename T, size_t N>
struct ArrayTraits<T[N]> {
    static constexpr bool is_array = true;
    static constexpr size_t size = N;
    using element_type = T;
};

int main() {
    std::cout << "int: " << ArrayTraits<int>::is_array << std::endl;  // 0
    std::cout << "int[10]: " << ArrayTraits<int[10]>::is_array << std::endl;  // 1
    std::cout << "int[10] size: " << ArrayTraits<int[10]>::size << std::endl;  // 10
    
    return 0;
}

3. 실전 예제: 타입 트레이트

is_pointer 구현

#include <iostream>

// 일반 템플릿: 포인터 아님
template<typename T>
struct is_pointer {
    static constexpr bool value = false;
};

// 포인터 부분 특수화
template<typename T>
struct is_pointer<T*> {
    static constexpr bool value = true;
};

// const 포인터 부분 특수화
template<typename T>
struct is_pointer<T* const> {
    static constexpr bool value = true;
};

int main() {
    std::cout << "int: " << is_pointer<int>::value << std::endl;           // 0
    std::cout << "int*: " << is_pointer<int*>::value << std::endl;         // 1
    std::cout << "int* const: " << is_pointer<int* const>::value << std::endl;  // 1
    std::cout << "const int*: " << is_pointer<const int*>::value << std::endl;  // 1
    
    return 0;
}

remove_const 구현

#include <iostream>
#include <type_traits>

// 일반 템플릿: const 아님
template<typename T>
struct remove_const {
    using type = T;
};

// const 전문화
template<typename T>
struct remove_const<const T> {
    using type = T;
};

int main() {
    std::cout << std::is_same_v<remove_const<int>::type, int> << std::endl;  // 1
    std::cout << std::is_same_v<remove_const<const int>::type, int> << std::endl;  // 1
    std::cout << std::is_same_v<remove_const<const int*>::type, const int*> << std::endl;  // 1
    
    return 0;
}

4. 자주 발생하는 문제

문제 1: 함수 템플릿 부분 특수화 불가

// ❌ 함수 템플릿 부분 특수화 (불가)
template<typename T>
void func(T value) {
    std::cout << "Generic" << std::endl;
}

// 컴파일 에러!
// template<typename T>
// void func<T*>(T* value) {
//     std::cout << "Pointer" << std::endl;
// }

// ✅ 오버로드 사용
template<typename T>
void func(T value) {
    std::cout << "Generic" << std::endl;
}

template<typename T>
void func(T* value) {
    std::cout << "Pointer" << std::endl;
}

int main() {
    int x = 10;
    func(x);   // Generic
    func(&x);  // Pointer
    
    return 0;
}

문제 2: 선택 우선순위

#include <iostream>

// 일반 템플릿
template<typename T>
struct Test {
    static void print() { std::cout << "Generic" << std::endl; }
};

// 포인터 부분 특수화
template<typename T>
struct Test<T*> {
    static void print() { std::cout << "Pointer" << std::endl; }
};

// const 포인터 부분 특수화
template<typename T>
struct Test<const T*> {
    static void print() { std::cout << "Const Pointer" << std::endl; }
};

// int* 전문화
template<>
struct Test<int*> {
    static void print() { std::cout << "Int Pointer" << std::endl; }
};

int main() {
    Test<int>::print();        // Generic
    Test<double*>::print();    // Pointer
    Test<const int*>::print(); // Const Pointer
    Test<int*>::print();       // Int Pointer (가장 구체적)
    
    return 0;
}

우선순위:

  1. 전문화 (Test<int*>)
  2. 부분 특수화 (Test<const T*>, Test<T*>)
  3. 일반 템플릿 (Test<T>)

문제 3: ODR 위반

// ❌ 헤더에 특수화 정의 (ODR 위반 가능)
// header.h
template<>
void func<int>(int value) {  // 여러 TU에서 포함 시 중복 정의
    // ...
}

// ✅ 헤더에 선언, 소스에 정의
// header.h
template<typename T>
void func(T value);

template<>
void func<int>(int value);

// source.cpp
template<>
void func<int>(int value) {
    // ...
}

문제 4: 부분 특수화 모호성

#include <iostream>

template<typename T, typename U>
struct Pair {
    static void print() { std::cout << "Generic" << std::endl; }
};

// 부분 특수화 1
template<typename T>
struct Pair<T, T> {
    static void print() { std::cout << "Same Type" << std::endl; }
};

// 부분 특수화 2
template<typename T>
struct Pair<T, int> {
    static void print() { std::cout << "Second is int" << std::endl; }
};

int main() {
    Pair<double, float>::print();  // Generic
    Pair<int, int>::print();       // 모호성! (1과 2 모두 매칭)
    
    return 0;
}

해결:

// 전문화로 명시
template<>
struct Pair<int, int> {
    static void print() { std::cout << "Both int" << std::endl; }
};

5. 실전 예제: 직렬화 시스템

#include <iostream>
#include <string>
#include <vector>
#include <sstream>

// 일반 템플릿
template<typename T>
struct Serializer {
    static std::string serialize(const T& value) {
        std::ostringstream oss;
        oss << value;
        return oss.str();
    }
};

// std::string 전문화
template<>
struct Serializer<std::string> {
    static std::string serialize(const std::string& value) {
        return "\"" + value + "\"";
    }
};

// bool 전문화
template<>
struct Serializer<bool> {
    static std::string serialize(const bool& value) {
        return value ? "true" : "false";
    }
};

// vector 부분 특수화
template<typename T>
struct Serializer<std::vector<T>> {
    static std::string serialize(const std::vector<T>& vec) {
        std::string result = "[";
        for (size_t i = 0; i < vec.size(); ++i) {
            if (i > 0) result += ", ";
            result += Serializer<T>::serialize(vec[i]);
        }
        result += "]";
        return result;
    }
};

// 포인터 부분 특수화
template<typename T>
struct Serializer<T*> {
    static std::string serialize(T* const& ptr) {
        if (ptr) {
            return "*" + Serializer<T>::serialize(*ptr);
        }
        return "null";
    }
};

int main() {
    std::cout << Serializer<int>::serialize(42) << std::endl;  // 42
    std::cout << Serializer<std::string>::serialize("hello") << std::endl;  // "hello"
    std::cout << Serializer<bool>::serialize(true) << std::endl;  // true
    
    std::vector<int> v = {1, 2, 3};
    std::cout << Serializer<std::vector<int>>::serialize(v) << std::endl;  // [1, 2, 3]
    
    std::vector<std::string> vs = {"a", "b", "c"};
    std::cout << Serializer<std::vector<std::string>>::serialize(vs) << std::endl;  // ["a", "b", "c"]
    
    int x = 100;
    std::cout << Serializer<int*>::serialize(&x) << std::endl;  // *100
    std::cout << Serializer<int*>::serialize(nullptr) << std::endl;  // null
    
    return 0;
}

출력:

42
"hello"
true
[1, 2, 3]
["a", "b", "c"]
*100
null

6. 실전 예제: 타입 트레이트 라이브러리

#include <iostream>
#include <type_traits>

// TypeTraits: 타입 정보 제공
template<typename T>
struct TypeTraits {
    static constexpr bool is_pointer = false;
    static constexpr bool is_const = false;
    static constexpr bool is_array = false;
    static constexpr size_t size = sizeof(T);
    
    static std::string name() { return "Unknown"; }
};

// 포인터 부분 특수화
template<typename T>
struct TypeTraits<T*> {
    static constexpr bool is_pointer = true;
    static constexpr bool is_const = false;
    static constexpr bool is_array = false;
    static constexpr size_t size = sizeof(void*);
    
    static std::string name() { 
        return TypeTraits<T>::name() + "*"; 
    }
};

// const 부분 특수화
template<typename T>
struct TypeTraits<const T> {
    static constexpr bool is_pointer = TypeTraits<T>::is_pointer;
    static constexpr bool is_const = true;
    static constexpr bool is_array = TypeTraits<T>::is_array;
    static constexpr size_t size = sizeof(T);
    
    static std::string name() { 
        return "const " + TypeTraits<T>::name(); 
    }
};

// 배열 부분 특수화
template<typename T, size_t N>
struct TypeTraits<T[N]> {
    static constexpr bool is_pointer = false;
    static constexpr bool is_const = false;
    static constexpr bool is_array = true;
    static constexpr size_t size = sizeof(T) * N;
    
    static std::string name() { 
        return TypeTraits<T>::name() + "[" + std::to_string(N) + "]"; 
    }
};

// int 전문화
template<>
struct TypeTraits<int> {
    static constexpr bool is_pointer = false;
    static constexpr bool is_const = false;
    static constexpr bool is_array = false;
    static constexpr size_t size = sizeof(int);
    
    static std::string name() { return "int"; }
};

// double 전문화
template<>
struct TypeTraits<double> {
    static constexpr bool is_pointer = false;
    static constexpr bool is_const = false;
    static constexpr bool is_array = false;
    static constexpr size_t size = sizeof(double);
    
    static std::string name() { return "double"; }
};

template<typename T>
void printTraits() {
    std::cout << "Type: " << TypeTraits<T>::name() << std::endl;
    std::cout << "  Pointer: " << TypeTraits<T>::is_pointer << std::endl;
    std::cout << "  Const: " << TypeTraits<T>::is_const << std::endl;
    std::cout << "  Array: " << TypeTraits<T>::is_array << std::endl;
    std::cout << "  Size: " << TypeTraits<T>::size << " bytes" << std::endl;
    std::cout << std::endl;
}

int main() {
    printTraits<int>();
    printTraits<int*>();
    printTraits<const int>();
    printTraits<const int*>();
    printTraits<int[10]>();
    printTraits<double>();
    
    return 0;
}

출력:

Type: int
  Pointer: 0
  Const: 0
  Array: 0
  Size: 4 bytes

Type: int*
  Pointer: 1
  Const: 0
  Array: 0
  Size: 8 bytes

Type: const int
  Pointer: 0
  Const: 1
  Array: 0
  Size: 4 bytes

Type: const int*
  Pointer: 1
  Const: 1
  Array: 0
  Size: 8 bytes

Type: int[10]
  Pointer: 0
  Const: 0
  Array: 1
  Size: 40 bytes

Type: double
  Pointer: 0
  Const: 0
  Array: 0
  Size: 8 bytes

정리

핵심 요약

  1. 전문화: 한 타입에 대한 완전히 다른 구현
  2. 부분 특수화: 타입 패턴에 대한 다른 구현
  3. 클래스만: 부분 특수화는 클래스 템플릿만 가능
  4. 우선순위: 전문화 > 부분 특수화 > 일반 템플릿
  5. 실무: 타입 트레이트, 직렬화, 최적화

특수화 비교

특징전문화부분 특수화
대상함수/클래스 템플릿클래스 템플릿만
타입구체적 타입 (int, double)타입 패턴 (T*, const T)
문법template<>template<typename T>
예시Storage<bool>Storage<T*>
용도특정 타입 최적화패턴별 동작

실전 팁

설계 원칙:

  • 일반 템플릿: 기본 동작
  • 부분 특수화: 패턴별 동작
  • 전문화: 특정 타입 최적화
  • 오버로드: 함수 템플릿 분기

성능:

  • 타입별 최적화 가능
  • 컴파일 타임 분기
  • 런타임 오버헤드 없음
  • 코드 크기 증가 주의

주의사항:

  • 함수 템플릿은 전문화만
  • ODR 위반 주의
  • 모호성 해결 (전문화 추가)
  • 선택 우선순위 이해

다음 단계

  • C++ Template Basics
  • C++ SFINAE
  • C++ Type Traits

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 템플릿 | “제네릭 프로그래밍” 초보자 가이드
  • C++ 템플릿 특수화 완벽 가이드 | 완전·부분 특수화, 문제 시나리오, 프로덕션 패턴
  • C++ SFINAE | “Substitution Failure Is Not An Error” 가이드

이 글에서 다루는 키워드 (관련 검색어)

C++, template, specialization, partial specialization 등으로 검색하시면 이 글이 도움이 됩니다.

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ 템플릿 특수화. 전문화, 부분 특수화, 특정 타입에 대한 별도 구현으로 최적화·예외 처리. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


관련 글

  • C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
  • C++ CTAD |
  • C++20 Concepts 완벽 가이드 | 템플릿 제약의 새 시대
  • C++ constexpr if |
  • C++ CRTP 패턴 |