지난 포스팅에 이어서 클래스 템플릿에 대해 마저 정리해볼까 한다.
클래스 템플릿 특수화
클래스 템플릿 역시 함수 템플릿 처럼 특수화가 가능하다. 틀래스 템플릿을 특수화 시키는 방법은 다음과 같다.
1
2
3
4
template<>
class Stack<std::string> {
// ...
};
이때 클래스 템플릿을 특수화 하려면 모든 멤버 함수를 특수화해야 한다.
클래스 템플릿 특수화 시 모든 멤버 함수의 정의는 일반 멤버 함수처럼 정의되어야 하며, 타입 파라미터(T
) 대신 특수화된 타입을 사용해야 한다.
1
2
3
void Stack<std::string>::push(const std::string& e) {
// ...
}
클래스 템플릿 부분 특수화
클래스 템플릿은 부분적으로 특수화할 수 있다. 특정 환경에서만 필요한 구현을 명시할 수 있지만, 일부 템플릿 파라미터는 여전히 사용자가 지정해야만 한다.
다음은 포인터 타입을 위해 Stack<>
클래스를 부분 특수화한 코드다.
1
2
3
4
template<typename T>
class Stack<T*> {
// ...
}
위 코드는 타입이 T로 파라미터화되어 있기는 하지만, 포인터를 위해 특수화된 클래스 템플릿이다.
여러 템플릿 파라미터 사이의 관계를 특수화 시킬수도 있다.
1
2
3
4
template<typename T1, typename T2>
class MyClass {
// ...
};
위와 같은 클래스 템플릿이 있을 경우 다음과 같이 부분 특수화를 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 두 템플릿 파라미터의 타입이 같은 경우
template<typename T>
class MyClass<T, T> {
// ...
}
// 두 번째 파라미터 타입이 int형인 경우
template<typename T>
class MyClass<T, int> {
// ...
};
// 두 템플릿 파라미터의 타입이 모두 포인터형인 경우
template<typename T1, typename T2>
class MyClass<T1*, T2*> {
// ...
};
이때 주의해야할 점은 부분 특수화시 다음과 같은 경우 선언이 모호해 질 수 있다는 점이다.
1
2
MyClass<int, int> m; // Error: MyClass<T, T>, MyClass<T, int> 둘다 일치
MyClass<int*, int*> m; // Error: MyClass<T, T>, MyClass<T1*, T2*> 둘다 일치
기본 클래스 템플릿 인자
클래스 템플릿에서는 템플릿 파라미터의 기본값을 지정할 수 있다. 다음과 같이 Stack<>
에서 두 번째 템플릿 파라미터로 데이터를 관리할 컨테이너를 정의하되 std::vector<>
를 기본값으로 지정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T, typename Cont = std::vector<T>>
class Stack{
// ...
private:
Cont elems;
};
Stack<int> int_stack; // Int형 스택
// Double형 스택, 데이터를 deqeu으로 관리한다.
Stack<double, std::deque<double>> double_stack;
타입 별칭
타입에 새로운 이름을 부여하여 클래스 템플릿을 보다 간편하게 사용할 수 있다.
1
2
3
4
5
6
7
typedef Stack<int> IntStack;
// or
using IntStack = Stack<int>; // C++ 11 이상
void foo(const IntStack& s);
IntStack istack[10];
using
키워드는 C++11부터 지원하는 키워드로, typedef
와 같이 타입에 대한 새로운 이름을 부여한다. 하지만 using
사용시 아래와 같이 별칭 템플릿(alias template)을 사용할 수 있다.
1
2
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;
별칭 템플릿은 다음과 같이 활용할 수 있다.
1
2
3
4
5
6
7
8
9
struct MyType {
using iterator = ...;
};
template<typename T>
using MyTypeIterator = typename MyType<T>::iterator;
MyTypeIterator<int> pos;
c++14부터 타입 트레잇(표준 라이브러리) 내에 모든 타입에 대해 다음과 같은 별칭을 제공한다.
1
2
3
4
5
6
7
8
std::add_const_t<T> // c++14 이상
typename std::add_const<T>::type // c++11
// 표준 라이브러리에는 이런식으로 정의되어 있음
namespace std {
template<typename T> using add_const_t = typename add_const<T>::type;
}
클래스 템플릿 인자 연역
c++17 이전에는 클래스 템플릿을 사용할 때 모든 템플릿 파라미터 타입을 명시해야 했다.(기본값이 있는 경우 제외). c++17 부터는 템플릿 인자를 명시적으로 표시할 필요가 없어졌다. 물론 생성자를 통해 모든 템플릿 파라미터를 연역할 수 있는 경우에만 해당된다.
1
2
3
Stack<int> istack1;
Stack<int> istack2 = istack1;
Stack istack3 = istack1; // c++17 이상
다음과 같이 하나의 요소로 초기화된 스택을 제공한다고 하자.
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class Stack{
private:
std::vector<T> elems;
public:
Stack() = default;
Stack(const T& e) : elems({e}) {}
};
Stack int_stack = 0; // c++17 이상, Stack<int>으로 연역됨
스택을 정수형 값 0으로 초기화하면 템플릿 파라미터 T를 int로 연역할 수 있으므로 Stack
문자열 값과 템플릿 인자 연역
원칙적으로 문자열 값으로도 해당 클래스를 초기화할 수 있다.
1
Stack string_stack = "bottom"; // Stack<const char[7]>
일반적으로 템플릿 형식 T의 인자를 레퍼런스 형태로 전달하면 파라미터에 타입 소실(decay)이 일어나지 않는다. (타입 소실을 통해 원시 배열을 원시 포인터 형으로 바꾸는 매커니즘이 수행된다.)
하지만 위 코드는 T가 const char*가 아닌 const char[7]로 연역하게 되므로, 크기가 다른 문자열은 해당 스택에 저장할 수 없다.
그로므로 이를 해결하기 위해서는 생성자를 값으로 받아야 한다.
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class Stack{
private:
std::vector<T> elems;
public:
Stack() = default;
Stack(T e) : elems({std::move(e)}) {}
};
Stack string_stack = "bottom"; // Stack<const char*>
연역 가이드
컨테이너 내에서 원시 포인터를 다루는 것은 좋지 않다. (여러모로) 이때 다음과 같이 연역 가이드 (deduction guide)를 통해 문자열 리터럴 혹은 C 문자열이 전달되면 std::string으로 인스턴스화하도록 정의할 수 있다.
1
2
3
4
5
6
7
8
template<typename T>
class Stack{
// ...
Stack(const T& e) : elems({e}) {}
};
Stack(const char*) -> Stack<std::string>;
이후 다음과 같이 선언하면 Stack<std::string>
이 인스턴스화된다.
1
2
3
Stack string_stack{"bottom"}; // Stack<std::string>
Stack string_stack = "bottom" // Stack<std::string>
하지만 C++ 문법상 std::string을 기대하는 생성자에 문자열 리터럴을 통한 복사 생성(=)을 수행할 수가 없다… 그러므로 이 이 스택은 위와 같은 방법으로 초기화되어야 한다.
템플릿화된 집합
집합(aggregate) 클래스도 템플릿이 될 수 있다. 다음 예를 살펴보자.
1
2
3
4
5
6
7
8
9
template<typename T>
struct ValueWithComment {
T value;
std::string comment;
};
ValueWithComment<int> vc;
vc.value = 42;
vc.comment = "initial value";
또한 집합 클래스 템플릿에 대해서도 연역 가이드를 정의할 수 있다. (c++17이상)
1
2
3
ValueWithComment(const char*, const char*) -> ValueWithComment<std::string>;
ValueWithComment vc2 = {"hello", "initial value"};
만일 연역 가이드가 없다면 연역을 수행할 생성자가 없어서 위와 같은 초기화가 불가능하다.