앞서 포스팅한 함수 템플릿과 클래스 템플릿에서는 타입에 대한 템플릿 파라미터를 설명했었다. 하지만 템플릿 파라미터는 타입이 아닌 일반 값 또한 사용 가능하다.
이번 포스팅에선 타입이 아닌(Non-type) 템플릿 파라미터에 대해 다뤄볼까 한다.
타입이 아닌 클래스 템플릿 파라미터
기존 Stack<T>
클래스에서 최대 사이즈를 템플릿을 통해 명시할 경우 다음과 같이 클래스를 수정할 수 있다. (Stack<>는 이전 포스팅에서 다룬 샘플 클래스 템플릿이다.)
1
2
3
4
5
6
7
8
9
template<typename T, std::size_t MaxSize>
class Stack {
// ...
};
Stack<int, 20> i20_stack;
Stack<int, 40> i40_stack;
Stack<std::string, 40> str_stack;
이때
Stack<int, 20>
과Stack<int, 40>
은 서로 다른 타입으로 인스턴스화 된다.
그러므로 둘 사이의 형변환이나 할당 역시 불가능하다.
마찬가지로 기본값 또한 명시할 수 있다.
1
2
3
4
template<typename T, std::size_t MaxSize = 100>
class Stack {
// ...
};
타입이 아닌 함수 템플릿 파라미터
함수 템플릿에도 타입이 아닌 템플릿 파라미터를 사용할 수 있다.
1
2
3
4
template<int Val, typename T>
T addValue(T x) {
return x + Val;
}
이런류의 함수는 보통 특정 함수나 연산이 파라미터로 사용될때 유용하다.
1
std::transform(src.begin(), src.end(), dst.begin(), addValue<5, int>);
이때 전달된 값을 통해 T를 연역하기 위해 다음과 같이 함수 템플릿을 수정할 수 있다.
1
2
template<auto Val, typename T = decltype(Val)>
T foo();
혹은 전달된 값이 전달된 타입과 같은 타입을 갖게 강제할 수도 있다.
1
2
template<typanem T, T Val = T{}>
T bar();
각종 제약 사항
타입이 아닌 템플릿 파라미터에는 몇 가지 제약 사항이 있다.
일반적으로 정수 상수값(열거형 포함), 객체/함수/멤버에 대한 포인터, 객체나 함수에 대한 좌측값 참조 또는 std::nullptr_t
(nullptr
의 타입) 이어야 한다.
부동소수점(float, double) 혹은 클래스 타입은 템플릿 파라미터로 사용될 수 없다.
1
2
3
4
5
6
7
8
9
template<double VAT>
double process(double v) { // 에러, double은 템플릿 파라미터로 사용 불가
return v * VAT;
}
template<std::string name> // 에러, 클래스 타입은 템플릿 파라미터로 사용 불가
class MyClass {
// ...
};
포인터나 레퍼런스를 템플릿 파라미터로 전달할 때 객체는 문자열 리터럴, 임시 객체, 데이터 멤버 혹은 타 하위 객체여서는 안된다.
1
2
3
4
5
template<const char* name>
class MyClass {
// ...
};
MyClass<"hello"> x; // 에러, 문자열 리터럴은 사용할 수 없다.
문자열의 경우 포인터 타입이 아닌 상수 배열 형태로는 템플릿 파라미터로 전달할 수 있으며 해당 내용은 해당 포스팅의 ‘auto 타입’ 항목에서 다룬다.
유효하지 않은 표현식
타입이 아닌 템플릿 파라미터는 컴파일 과정 표현식이기만 하면 된다.
1
2
3
4
template<int I, bool B>
class C;
//...
C<sizeof(int)+4, sizeof(int)==4> c;
>
연산을 템플릿 파라미터의 표현식에서 사용하려면 >
때문에 파라미터 목록이 끝나지 않게 전체 표현식을 괄호로 감싸야 한다.
1
2
C<42, sizeof(int) > 4> c; // 에러
C<42, (sizeof(int) > 4)> c;
auto 타입
c++17부터는 타입이 아닌 템플릿 파라미터로 허용된 어떠한 형식이든 받아들일 수 있게 정의할 수 있다.
1
2
3
4
5
template<typename T, auto MaxSize>
class Stack {
using size_type = decltype(MaxSize);
// ...
};
위처럼 플레이스홀더 타입(placeholder type) auto를 사용해 아직 명시되지 않은 타입을 갖는 값으로 MaxSize
를 정의할 수 있다.
1
2
3
Stack<int, 20> i20stack; // size_type == int
Stack<int, 40u> i40stack; // size_type == unsigned int
Stack<int, 3.5> i3_5stack; // 에러
물론 앞서 설명했던 제약 조건에 따라 실수형 값은 사용할 수 없다.
또한 문자열은 상수 배열로 전달할 수 있기 때문에 다음 코드 역시 사용 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
template<auto T>
class Message {
public:
void print() {
std::cout << T << '\n';
}
};
int main() {
Message<42> msg1;
msg1.print();
static const char s[] = "hello";
Message<s> msg2; // T == "hello", Type == const char[6]
msg2.print();
}
문자열의 경우 포인터 타입이 아닌 상수 배열로 전달할 수 있으며 C++17까지 오면서 점점 이러한 제약 사항이 완화되고 있다.
1
2
3
4
5
6
7
8
9
10
extern const char s03[] = "hello" // 외부 링크
const char s11[] = "hello"; // 내부 링크
int main() {
Message<s03> m03; // 모든 버전 사용 가능
Message<s11> m11; // c++11 이상 사용 가능
static const char s17[] = "hello"; // 링크 없음
Message<s17> m17; // c++17 이상 사용 가능
}
객체가 외부 링크를 가졌다면 모든 c++ 버전에서 사용할 수 있다. 또한 c++11부터는 내부 링크만 있어도 사용 가능하며, c++17부터는 링크 없이도 사용 가능하다.
심지어 template<decltype(auto) N>
도 가능하다. N
을 레퍼런스로써 인스턴스화 시킬때 사용 가능하다.
1
2
3
4
5
6
7
template<decltype(auto) N>
class C{
// ...
};
int i;
C<(i)> x;
위 코드에서 N
의 타입은 int&
가 된다.
마치며
템플릿은 언제나 힘든 주제인것 같다. (아직 다뤄야할 것들이 많은데..)
다음 포스팅에서는 가변 인자 템플릿에 대해 다뤄볼까 한다.