템플릿 특수화
일반 템플릿이 맞지 않는 타입에 명시적·부분 특수화를 제공하고 중복과 유지보수 비용을 고려해 적용합니다.
템플릿은 대부분의 경우 매우 유용하지만, 때로는 특정 타입에 대해서는 템플릿의 일반적인 구현이 적절하지 않거나, 더 효율적인 특별한 동작을 제공하고 싶은 경우가 발생합니다.
이럴 때 사용하는 것이 바로 템플릿 특수화(Template Specialization)입니다.
이번 장에서는 템플릿 특수화의 개념과 사용법, 그리고 부분 특수화(Partial Specialization)와 명시적 특수화(Explicit Specialization 또는 Full Specialization)의 차이를 정리합니다.
C++ · SPECIALIZATION SELECTION
정확한 특수화 → 맞는 부분 패턴 → 기본 템플릿
본문 MyValue와 Pair 예제의 인수 조합을 비교합니다. 타입 이름뿐 아니라 어떤 정의를 선택하는지와 그 근거까지 함께 읽습니다.
| 사용 타입 | 선택하는 정의 | 선택 근거 |
|---|---|---|
MyValue<int> | 기본 MyValue<T> | 일치하는 명시적 특수화가 없습니다. |
MyValue<bool> | MyValue<bool> 명시적 특수화 | 정확한 bool 조합의 정의가 있습니다. |
MyValue<const char*> | MyValue<const char*> 명시적 특수화 | 문자열을 std::string 멤버로 관리하는 별도 정의입니다. |
Pair<double, char> | 기본 Pair<T1, T2> | 본문의 두 부분 특수화 패턴에 모두 맞지 않습니다. |
Pair<std::string, int> | Pair<T, int> 부분 특수화 | 두 번째 인수가 int입니다. |
Pair<int, int*> | Pair<T, T*> 부분 특수화 | 두 번째 인수는 첫 번째 타입의 포인터입니다. |
MyValue<int>
선택: 기본 MyValue<T>
근거: 일치하는 명시적 특수화가 없습니다.
MyValue<bool>
선택: MyValue<bool> 명시적 특수화
근거: 정확한 bool 조합의 정의가 있습니다.
MyValue<const char*>
선택: MyValue<const char*> 명시적 특수화
근거: 문자열을 std::string 멤버로 관리하는 별도 정의입니다.
Pair<double, char>
선택: 기본 Pair<T1, T2>
근거: 본문의 두 부분 특수화 패턴에 모두 맞지 않습니다.
Pair<std::string, int>
선택: Pair<T, int> 부분 특수화
근거: 두 번째 인수가 int입니다.
Pair<int, int*>
선택: Pair<T, T*> 부분 특수화
근거: 두 번째 인수는 첫 번째 타입의 포인터입니다.
패턴이 여러 개 일치하면: 부분 순서화로 유일한 최적 특수화를 정할 수 있어야 합니다. 우열이 없는 여러 패턴이 맞으면 모호성 오류가 납니다.
템플릿 특수화의 필요성
템플릿은 모든 타입에 대해 일반적인 방식으로 동작하도록 설계됩니다.
하지만 특정 타입의 특성 때문에 일반적인 템플릿 구현이 문제가 되거나, 혹은 더 나은 성능을 낼 수 있는 특별한 방법이 있을 수 있습니다.
예시std::string과 같은 문자열 타입은 일반적인char배열과 다르게 동작합니다.std::string의 복사는 문자열 전체를 복사하는 깊은 복사를 의미할 수 있습니다.bool타입은 메모리 효율성을 위해 특별하게 처리되는 경우가 많습니다. (예:std::vector<bool>)char*(C-스타일 문자열)와 같은 포인터 타입은 일반적인 포인터와는 다른 비교나 복사 의미를 가질 수 있습니다.
이러한 경우 특정 타입에 대해서만 템플릿의 동작을 재정의하는 것이 필요하며, 이것이 템플릿 특수화의 목적입니다.
함수 템플릿의 명시적 특수화
함수 템플릿은 특정 타입에 대해 완전히 다른 구현을 제공할 수 있습니다.
이를 함수 템플릿의 명시적 특수화라고 합니다. 앞 절의 타입 추론·오버로딩에 더해, 선택한 템플릿의 특정 인수 조합에 어떤 정의를 사용할지를 다룹니다. 아래의 한글 이름을 쓴 형식은 설명용이고 mySwap 예제는 별도 파일 하나로 실행하는 프로그램입니다.
template <> // 템플릿 매개변수 리스트는 비워둡니다.
반환타입 함수이름<특수화할_타입>(매개변수) {
// 특수화된 타입에 대한 특별한 구현
}std::swap은 해당 오버로드의 요구 조건을 만족하는 타입에 사용할 수 있습니다. 아래 mySwap은 특정 타입에 대한 특수화를 어떻게 하는지 보여주기 위한 예시입니다.
예를 들어 const char* (C-스타일 문자열)은 문자열 리터럴을 가리키는 경우가 많기 때문에, 소유권이 없는 포인터를 delete[] 하면 정의되지 않은 동작(UB)이 됩니다.
따라서 특수화 예시는 포인터 값만 안전하게 교환하는 방식으로 작성해야 합니다.
#include <iostream>
#include <string>
#include <typeinfo>
#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() 호출 (포인터 교환): ";
const char* temp = a;
a = b;
b = temp;
}
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-스타일 문자열은 소유권 규칙이 까다롭습니다.
// "내용 자체"를 교환/복사해야 한다면 const char* 대신 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;
}원 코드의 int 특수화는 주석으로 닫혀 있으므로 현재 실행에서는 int·double·std::string 모두 일반 mySwap을 사용합니다. const char* 호출만 명시적 특수화의 안내가 나옵니다. 결과 쌍은 20·10, 2.2·1.1, World·Hello, Banana·Apple 순서입니다. 마지막 두 경우 모두 출력할 문자열이 바뀌지만 포인터 특수화는 포인터 두 개를 교환하고, std::string 일반 템플릿은 값의 복사·대입을 수행합니다. 이 const char* 특수화도 일반 템플릿과 같은 포인터 교환 알고리즘이므로 특수화를 적었다는 이유만으로 성능 개선을 주장하지 않습니다.
함수 템플릿 특수화의 주의사항- 함수 템플릿 특수화는 오버로딩과 다릅니다. 명시적 특수화는 독립적인 오버로드 후보가 아닙니다. 먼저 기본 함수 템플릿을 선택하고 해당 인수 조합의 명시적 특수화가 있으면 그 정의를 사용합니다.
- 일반적으로 함수 템플릿 특수화보다는 함수 오버로딩을 선호합니다. 특정 타입에 대한 동작이 너무 달라 템플릿의 원래 의도에서 벗어나는 경우에만 특수화를 고려합니다.
- 후보 참여 조건에는 C++11의
std::enable_if를 이용한 SFINAE나 C++20 제약을, 선택된 함수 본문의 컴파일 시간 분기에는 C++17의if constexpr를 검토할 수 있습니다.if constexpr자체는 SFINAE가 아닙니다.
C++ · OVERLOAD OR SPECIALIZATION
후보 추가, 본문 분기, 특정 구현 대체를 구분한다
함수 템플릿에는 partial specialization이 없다. 호출 후보를 추가할지 primary의 한 specialization을 대체할지 구분한다.
- 호출 형태가 다름
overload로 후보를 명시한다.
- 작은 타입 분기
if constexpr가 더 가까이 읽히는지 본다.
- 정확한 한 타입
primary specialization 대체가 필요할 때만 명시 특수화한다.
명시적 함수 특수화는 독립 overload 후보가 아니라 선택된 primary specialization의 구현 대체다.
클래스 템플릿의 명시적 특수화
클래스 템플릿도 특정 타입에 대해 완전히 다른 구현을 제공할 수 있습니다.
이를 클래스 템플릿의 명시적 특수화라고 합니다.
template <>
class 클래스이름<특수화할_타입> {
// 특수화된 타입에 대한 특별한 멤버 변수 및 멤버 함수 구현
};일반적으로 MyValue 템플릿은 어떤 타입이든 저장하지만, bool 타입에 대해서는 다른 저장·표시 방식을 적용해 봅시다. 이 예제의 int 저장은 메모리 절약 예제가 아닙니다. 비트를 압축하는 std::vector<bool>과 구별합니다.
#include <iostream>
#include <string>
#include <typeinfo>
// 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:
// 값의 표현 방식을 바꾸는 예시이며 메모리 절약을 보장하지 않음
// 여기서는 간단히 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. const char* 타입에 대한 명시적 특수화
// 내부 저장은 std::string으로 관리하여 수동 메모리 관리 위험을 제거
template <>
class MyValue<const char*> {
private:
std::string str;
public:
explicit MyValue(const char* s) {
std::cout << "const char* 명시적 특수화 MyValue<const char*> 객체 생성.\n";
str = (s ? s : "");
}
const char* getValue() const { return str.c_str(); }
void setValue(const char* s) {
str = (s ? s : "");
}
void display() const {
std::cout << "const char* 특수화 MyValue 값: " << str << 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* 특수화는 내부적으로 std::string을 사용해 수동 메모리 해제 없이 안전하게 문자열을 관리합니다. 생성할 때 입력 문자열을 내부 값으로 복사하지만 getValue의 반환값은 그 문자열을 가리키는 비소유 포인터입니다. 이 포인터를 delete하지 않으며, setValue로 문자열을 변경하거나 객체가 파괴된 뒤에도 보관한 포인터를 계속 쓸 수 있다고 가정하지 않습니다. 변경 뒤에는 getValue를 다시 호출하고, 독립적으로 오래 보관할 값이 필요하면 std::string으로 복사하세요.
스스로 확인하기 위 MyValue 정의는 유지하고 별도 main에서 아래를 실행하기 전에, 첫 display의 문자열과 마지막 display의 문자열을 예측하세요. input과 내부 str이 같은 배열이라고 가정하지 마세요.
int main() {
char input[] = "Sun";
MyValue<const char*> word(input);
input[0] = 'F';
word.display();
word.setValue(nullptr);
word.display();
}복사한 값과 빌린 포인터 풀이
입력 배열은 Fun으로 바뀌지만 word는 생성 시 복사한 Sun을 출력합니다. nullptr를 setValue에 주면 이 특수화의 조건식이 빈 문자열을 저장하므로 마지막 display의 값 부분은 비어 있습니다. 입력 포인터 자체를 멤버로 저장하는 구현과 다릅니다. getValue에서 얻은 주소를 유지한 채 setValue 후 역참조하는 실험은 하지 않습니다.
클래스 템플릿의 부분 특수화
클래스 템플릿은 모든 템플릿 매개변수를 특수화하지 않고, 일부만 특수화하는 것도 가능합니다.
이를 부분 특수화(Partial Specialization)라고 합니다.
함수 템플릿은 부분 특수화를 지원하지 않습니다.
template <typename T> // 하나의 매개변수 T만 남겨둠
class 클래스이름<T, 다른_타입> { // 두 번째 매개변수를 특정 타입으로 고정
// T 타입에 대한 부분 특수화된 구현
};
template <typename T>
class 클래스이름<T*> { // T 타입 포인터에 대한 부분 특수화
// T* 타입에 대한 부분 특수화된 구현
};Pair<T1, T2> 템플릿에서 두 번째 타입이 int인 경우와 첫 번째 타입의 포인터인 경우에 대해 부분 특수화를 해 봅시다. 부분 특수화는 단순히 일부 인수를 고정하는 경우뿐 아니라 T와 T*처럼 인수 사이의 관계를 지정할 수도 있습니다.
#include <iostream>
#include <string>
#include <typeinfo>
// 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 : T{}) << " (포인터 특수화)\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;
}컴파일러는 객체 생성 시점에 사용 가능한 템플릿 중에서 가장 특수화된 버전을 선택합니다.
정확한 인수 조합의 명시적 특수화가 있으면 그것을 사용합니다. 그렇지 않으면 일치하는 부분 특수화 중 부분 순서화로 하나를 선택하고, 일치하는 것이 없을 때 기본 템플릿을 사용합니다. 여러 패턴 중 우열을 정할 수 없으면 오류입니다.
스스로 확인하기
위 Pair 정의 세 개에 대해 Pair<int, int>, Pair<double, double*>, Pair<int, double*>가 각각 어느 정의를 사용하는지 결정하세요. 마지막 조합은 두 번째 인수가 포인터라는 이유만으로 T·T* 패턴에 맞을까요? 이어 원 main의 val을 200으로 바꾸고 p3를 다시 display하면 Value와 Ptr는 각각 무엇일까요?
패턴 일치와 저장 방식 풀이
순서대로 Pair<T, int>, Pair<T, T*>, 일반 Pair<T1, T2>입니다. 마지막 조합은 첫 인수에서 T=int, 두 번째에서 T=double을 동시에 요구하므로 T·T* 패턴에 맞지 않습니다. 원 p3에는 value 값 50이 복사되어 있고 ptr은 val을 빌려 가리키므로, val을 200으로 바꾸면 Value=50, Ptr=200입니다. 포인터가 가리키는 객체는 display할 때까지 살아 있어야 하며, 이 Pair가 val의 수명을 소유하거나 늘리는 것은 아닙니다. null 포인터를 주면 원 구현의 조건식이 T{} 값을 표시하므로 역참조하지 않습니다.
예시
다음은 타입 선택만 확인하는 별도 프로그램입니다. 두 부분 특수화가 각각 한 자리를 고정했으므로 Choice<int, int>에서는 둘 다 맞으며 어느 한쪽이 더 구체적이지 않습니다.
template <typename A, typename B>
struct Choice {};
template <typename T>
struct Choice<T, int> {};
template <typename U>
struct Choice<int, U> {};
int main() {
Choice<int, int> ambiguous;
}이 프로그램은 컴파일 오류가 예상되며 실행 출력은 없습니다. Choice<double, int>로 바꾸면 첫 번째 부분 특수화만 일치해 컴파일됩니다. 두 부분 특수화 아래, main 앞에 template <> struct Choice<int, int> {};를 추가하면 정확한 조합의 완전 특수화를 제공한 별도 변형도 컴파일됩니다. “특수화가 여러 개 있으면 마지막 선언이 이긴다”는 규칙은 아닙니다.
템플릿 특수화 사용 시 고려사항
- 남용 금지: 템플릿 특수화는 표현력이 큰 기능이지만, 너무 많이 사용하면 코드의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다. 일반적인 템플릿으로 해결할 수 있다면 특수화보다는 일반적인 구현을 선호하는 것이 좋습니다.
- 오버로딩 vs 특수화: 함수 템플릿의 경우, 특정 타입에 대해 다른 동작을 제공할 때 함수 오버로딩과 특수화를 모두 사용할 수 있습니다. 일반적으로 함수 오버로딩이 더 선호됩니다. 특수화는 템플릿의 기본 동작을 대체하는 것이고, 오버로딩은 단순히 추가적인 함수를 제공하는 것으로 이해할 수 있습니다.
- 정의 배치: 부분 특수화의 정의는 인스턴스화 지점에서 보여야 합니다. 완전 특수화된 함수는 헤더에 선언하고 한 소스 파일에서 정의하거나, 헤더 정의에 inline을 사용할 수 있습니다. 여러 번역 단위에 비-inline 함수 정의를 복제하면 ODR을 위반합니다. 완전 특수화 클래스도 일반 클래스처럼 멤버 함수의 선언과 정의를 분리할 수 있습니다.
특수화는 예외를 표현하는 도구이므로 “일반 템플릿으로 충분한가?”를 먼저 묻는 편이 좋습니다.
앞의 판단 흐름은 함수 오버로딩, 명시적 특수화, 부분 특수화를 구분하는 판단 흐름입니다. 이 세 선택은 한 프로그램에서 함께 사용할 수도 있습니다. SFINAE는 특정 템플릿의 대입 실패를 후보 제외로 처리하는 규칙이고, if constexpr는 선택된 본문의 컴파일 시간 분기입니다. 여기서는 용도를 구분하는 데까지만 다루며 앞선 초보 예제 실행에 두 도구를 구현할 필요는 없습니다.
명시적 특수화 선언은 그 특수화가 암시적으로 생성될 사용보다 앞서 도달 가능해야 합니다. 본문의 mySwap 특수화를 main 위에 둔 이유입니다. 명시적 특수화의 선언·사용 조건, 부분 특수화의 일치·모호성 규칙, 문자열 포인터 접근자를 필요할 때 함께 확인하세요.