템플릿 특수화
일반 템플릿이 맞지 않는 타입에 명시적·부분 특수화를 제공하고 중복과 유지보수 비용을 고려해 적용합니다.
템플릿은 대부분의 경우 매우 유용하지만, 때로는 특정 타입에 대해서는 템플릿의 일반적인 구현이 적절하지 않거나, 더 효율적인 특별한 동작을 제공하고 싶은 경우가 발생합니다.
이럴 때 사용하는 것이 바로 템플릿 특수화(Template Specialization)입니다.
이번 장에서는 템플릿 특수화의 개념과 사용법, 그리고 부분 특수화(Partial Specialization)와 명시적 특수화(Explicit Specialization 또는 Full Specialization)의 차이를 정리합니다.
| 사용 타입 | 선택하는 정의 | 이유 |
|---|---|---|
MyValue<int> | 기본 MyValue<T> | 일치하는 명시적 특수화 없음 |
MyValue<bool> | 명시적 특수화 | 정확히 bool 조합을 정의 |
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-스타일 문자열)와 같은 포인터 타입은 일반적인 포인터와는 다른 비교나 복사 의미를 가질 수 있습니다.
이러한 경우 특정 타입에 대해서만 템플릿의 동작을 재정의하는 것이 필요하며, 이것이 템플릿 특수화의 목적입니다.
함수 템플릿의 명시적 특수화
함수 템플릿은 특정 타입에 대해 완전히 다른 구현을 제공할 수 있습니다.
이를 함수 템플릿의 명시적 특수화라고 합니다.
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;
}- 함수 템플릿 특수화는 오버로딩과 다릅니다. 명시적 특수화는 독립적인 오버로드 후보가 아닙니다. 먼저 기본 함수 템플릿을 선택하고 해당 인수 조합의 명시적 특수화가 있으면 그 정의를 사용합니다.
- 일반적으로 함수 템플릿 특수화보다는 함수 오버로딩을 선호합니다. 특정 타입에 대한 동작이 너무 달라 템플릿의 원래 의도에서 벗어나는 경우에만 특수화를 고려합니다.
- 후보 참여 조건에는 C++11의
std::enable_if를 이용한 SFINAE나 C++20 제약을, 선택된 함수 본문의 컴파일 시간 분기에는 C++17의if constexpr를 검토할 수 있습니다.if constexpr자체는 SFINAE가 아닙니다.
함수 템플릿에는 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을 사용해 수동 메모리 해제 없이 안전하게 문자열을 관리합니다.
클래스 템플릿의 부분 특수화
클래스 템플릿은 모든 템플릿 매개변수를 특수화하지 않고, 일부만 특수화하는 것도 가능합니다.
이를 부분 특수화(Partial Specialization)라고 합니다.
함수 템플릿은 부분 특수화를 지원하지 않습니다.
template <typename T> // 하나의 매개변수 T만 남겨둠
class 클래스이름<T, 다른_타입> { // 두 번째 매개변수를 특정 타입으로 고정
// T 타입에 대한 부분 특수화된 구현
};
template <typename T>
class 클래스이름<T*> { // T 타입 포인터에 대한 부분 특수화
// T* 타입에 대한 부분 특수화된 구현
};Pair<T1, T2> 템플릿에서 두 번째 타입이 int인 경우와 포인터인 경우에 대해 부분 특수화를 해봅시다.
#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;
}컴파일러는 객체 생성 시점에 사용 가능한 템플릿 중에서 가장 특수화된 버전을 선택합니다.
정확한 인수 조합의 명시적 특수화가 있으면 그것을 사용합니다. 그렇지 않으면 일치하는 부분 특수화 중 부분 순서화로 하나를 선택하고, 일치하는 것이 없을 때 기본 템플릿을 사용합니다. 여러 패턴 중 우열을 정할 수 없으면 오류입니다.
템플릿 특수화 사용 시 고려사항
- 남용 금지: 템플릿 특수화는 표현력이 큰 기능이지만, 너무 많이 사용하면 코드의 복잡성을 증가시키고 유지보수를 어렵게 만듭니다. 일반적인 템플릿으로 해결할 수 있다면 특수화보다는 일반적인 구현을 선호하는 것이 좋습니다.
- 오버로딩 vs 특수화: 함수 템플릿의 경우, 특정 타입에 대해 다른 동작을 제공할 때 함수 오버로딩과 특수화를 모두 사용할 수 있습니다. 일반적으로 함수 오버로딩이 더 선호됩니다. 특수화는 템플릿의 기본 동작을 대체하는 것이고, 오버로딩은 단순히 추가적인 함수를 제공하는 것으로 이해할 수 있습니다.
- 정의 배치: 부분 특수화의 정의는 인스턴스화 지점에서 보여야 합니다. 완전 특수화된 함수는 헤더에 선언하고 한 소스 파일에서 정의하거나, 헤더 정의에 inline을 사용할 수 있습니다. 여러 번역 단위에 비-inline 함수 정의를 복제하면 ODR을 위반합니다. 완전 특수화 클래스도 일반 클래스처럼 멤버 함수의 선언과 정의를 분리할 수 있습니다.
특수화는 예외를 표현하는 도구이므로 “일반 템플릿으로 충분한가?”를 먼저 묻는 편이 좋습니다.
앞의 판단 흐름은 함수 오버로딩, 명시적 특수화, 부분 특수화를 구분하는 판단 흐름입니다.