#19. [모던 C++] 개선된 템플릿(C++11, C++14, C++17, C++20)
- (C++11~) extern으로 템플릿 선언을 할 수 있으며, 템플릿 인스턴스 중복 생성을 없앨 수 있습니다.
- (C++11~) 템플릿 오른쪽 꺽쇠 괄호 파싱을 개선하여 템플릿 인스턴스화시
>
가 중첩되어>>
와 같이 되더라도 공백을 추가할 필요가 없습니다.- (C++14~) 변수 템플릿이 추가되어 변수도 템플릿으로 만들 수 있습니다.
- (C++17~) 클래스 템플릿 인수 추론이 추가되어 함수 템플릿처럼 템플릿 인스턴스화시 타입을 생략할 수 있습니다.
- (C++17~) 클래스 템플릿 인수 추론 사용자 정의 가이드가 추가되어 클래스 템플릿 인수 추론시 컴파일러에게 가이드를 줄 수 있습니다.
- (C++17~) 비타입 템플릿 인자에서 auto를 허용합니다.
- (C++20~) 축약된 함수 템플릿이 추가되어 함수 인자로 auto를 사용할 수 있습니다. 사실상 함수 템플릿의 간략한 표현입니다.
- (C++20~) 비타입 템플릿 인자 규칙이 완화되어 실수 타입과 리터럴 타입을 사용할 수 있습니다.
- (C++20~) 클래스 템플릿 인수 추론시 initializer_list인 경우가 개선되어
std::vector v_20{1, 2, 3};
처럼 템플릿 인자를 명시하지 않아도 됩니다.
extern 템플릿
기존에는 템플릿을 헤더 파일에 정의해 두고, 여러 cpp 파일에서 #include 했었는데요, 이러면 템플릿 정의가 중복되어 코드 크기가 커집니다.(템플릿 인스턴스 중복 생성 참고)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ----
// Test_ModernCpp_ExternTemplate.h 에서
// ----
namespace ExternTemplate {
template<typename T>
T Add(T a, T b) {
return a + b;
}
}
// ----
// test1.cpp 에서
// ----
#include "Test_ModernCpp_ExternTemplate.h"
TEST(TestModern, ExternTemplate1) {
using namespace ExternTemplate;
EXPECT_TRUE(Add(1, 2) == 3); // Add<int>()가 정의 되어 포함됩니다.
}
// ----
// test2. cpp 에서
// ----
#include "Test_ModernCpp_ExternTemplate.h"
TEST(TestModern, ExternTemplate2) {
using namespace ExternTemplate;
EXPECT_TRUE(Add(10, 20) == 30); // Add<int>가 재정의 되어 포함됩니다.
}
export 템플릿이 있기는 했지만, 제대로 구현한 컴파일러도 드물고 의견도 일치하지 않아 C++11 부터 완전히 remove되었고,
C++11 부터는 extern 템플릿을 추가하여 템플릿 선언만 할 수 있으며, 불필요한 코드 크기를 최소화 할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// ----
// Test_ModernCpp_ExternTemplate.h 에서
// ----
namespace ExternTemplate {
template<typename T>
T Add(T a, T b) {
return a + b;
}
}
// ----
// test1.cpp 에서
// ----
#include "Test_ModernCpp_ExternTemplate.h"
TEST(TestModern, ExternTemplate1) {
using namespace ExternTemplate;
EXPECT_TRUE(Add(1, 2) == 3); // Add<int>()가 정의 되어 포함됩니다.
}
// ----
// test2. cpp 에서
// ----
#include "Test_ModernCpp_ExternTemplate.h"
// 템플릿 선언만 합니다. 이전에 정의된 템플릿을 사용합니다.
extern template int ExternTemplate::Add<int>(int, int); // C++11
TEST(TestModern, ExternTemplate2) {
using namespace ExternTemplate;
EXPECT_TRUE(Add(10, 20) == 30);
}
템플릿 오른쪽 꺽쇠 괄호
템플릿 인스턴스화시 템플릿 개체로 인스턴스화 하면 >
이 중첩됩니다.
기존에는 이게 비트 Right Shift 연산자 >>
로 파싱되어 컴파일 되지 않았습니다. 따라서, vector<A<int> >
와 같이 억지로 띄어쓰기를 했는데요(템플릿 파싱 오류 참고),
C++11 부터는 파싱을 개선하여 vector<A<int>>
와 같이 >>
을 사용해도 됩니다.
1
2
3
4
5
template<typename T>
class A {};
std::vector<A<int> > a; // C++03. 공백을 추가해야 했습니다.
std::vector<A<int>> b_11; // C++11/ 공백이 없어도 됩니다.
(C++14~) 변수 템플릿
기존의 템플릿은 클래스와 함수만 지원했는데요(템플릿 참고),
C++14 부터는 변수도 템플릿으로 만들 수 있습니다.
다음 예는 동일한 값을 지정한 pi
를 타입에 따라 다른 정밀도로 사용하는 예입니다.
1
2
3
4
5
6
7
8
template<class T>
// constexpr T pi_14{3.1415926535897932385L}; // (X) 컴파일 오류. 중괄호 초기화는 암시적 형변환이 안되서 = 로 초기화 합니다.
constexpr T pi_14 = 3.1415926535897932385L;
// pi_14를 타입에 따라 서로 다른 정밀도로 사용할 수 있습니다.
std::cout << "pi_14<double> : " << std::setprecision(10) << pi_14<double> << std::endl;
std::cout << "pi_14<float> : " << std::setprecision(10) <<pi_14<float> << std::endl;
std::cout << "pi_14<int> : " << std::setprecision(10) <<pi_14<int> << std::endl;
1
2
3
pi_14<double> : 3.141592654
pi_14<float> : 3.141592741 // double 보다 정밀도가 낮습니다.
pi_14<int> : 3
또한, 템플릿 특수화를 이용하여 템플릿 메타 프로그래밍에 활용할 수 있습니다.
다음은 템플릿 메타 프로그래밍에서 소개한 Factorial
을 변수 템플릿으로 구현한 예입니다.
1
2
3
4
5
6
7
8
9
// 1을 뺀 값을 재귀 호출합니다.
template<int val>
constexpr int factorial_14 = val * factorial_14<val - 1>;
// 1일때 특수화 버전. 더이상 재귀 호출을 안합니다.
template<>
constexpr int factorial_14<1> = 1;
EXPECT_TRUE(factorial_14<5> == 5 * 4 * 3 * 2 * 1);
(C++17~) 클래스 템플릿 인수 추론
기존에는 함수 템플릿만 인수를 추론하고 클래스 템플릿은 인수를 추론하지 않았는데요(함수 템플릿 인수 추론 참고),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
T Func(T a, T b) {return a + b;}
EXPECT_TRUE(Func(1, 2) == 3); // 인수 1, 2 로부터 추론해서 사용합니다.
template<typename T>
class A {
public:
explicit A(T val) {}
T Func(T a, T b) {return a + b;}
};
A<int> a{10}; // ~C++17 이전에는 타입을 명시해야 합니다.
EXPECT_TRUE(a.Func(1, 2) == 3);
C++17 부터는 클래스 템플릿도 생성자에 전달된 인수를 추론하므로 다음과 같이 사용할 수 있습니다.
1
2
A a_17{10}; // C++17 이후부터는 인수로부터 추론합니다.
EXPECT_TRUE(a_17.Func(1, 2) == 3);
(C++20~) 클래스 템플릿 인수 추론시 initializer_list인 경우가 개선되어
std::vector v_20{1, 2, 3};
처럼 템플릿 인자를 명시하지 않아도 됩니다.
(C++17~) 클래스 템플릿 인수 추론 사용자 정의 가이드
C++17 에서는 클래스 템플릿 인수 추론 사용자 정의 가이드가 추가되어 클래스 템플릿 인수 추론시 컴파일러에게 가이드를 줄 수 있습니다.
예를들어 다음의 클래스 A
는 타입이 T
로 동일한 인자 x
와 y
를 전달받습니다.
따라서, A a_17{1, 2};
는 A<int>
로 추론하여 사용할 수 있는데요, A b_17{1, 1.5};
는 컴파일 오류가 발생합니다. A<int, double>
로 추론되어 T
의 타입이 int
와 double
로 서로 다르니까요.
1
2
3
4
5
6
7
8
9
template<typename T>
class A {
public:
A(T x, T y) {}
};
A a_17{1, 2}; // A<int>. 인수로부터 인자를 추론합니다.
// A b_17{1, 1.5}; // (X) 컴파일 오류. A<int, double>은 안됩니다.
A<double> c{1, 1.5}; // 명시적으로 A<double>을 사용합니다.
이럴때 클래스 템플릿 인수 추론시 컴파일러에게 가이드를 줄 수 있습니다.
사용자 정의 가이드 형식은 다음과 같습니다.
1
2
template<템플릿 인자>
템플릿명(생성자 인자) -> 템플릿명<인스턴스화할 타입>;
다음 예는 상기 예에서 그냥 2번째 템플릿 인자 타입으로 클래스 템플릿 인수 추론을 하도록 가이드한 예입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class A {
public:
A(T x, T y) {}
};
// 클래스 템플릿 인수 추론 사용자 정의 가이드
template<typename T, typename U>
A(T x, U y) -> A<U>; // T와 U가 타입이 다르다면 U 타입으로 추론하세요.
A a_17{1, 2};
A b_17{1, 1.5}; // (O) 클래스 템플릿 인수 추론 사용자 정의 가이드에 의해 A<double>로 추론합니다.
A<double> c{1, 1.5};
(C++17~) 비타입 템플릿 인자의 auto 허용
C++17 부터는 비타입 템플릿 인자에서 auto를 허용합니다.
기존에는 임의 타입의 개체를 템플릿 인자로 사용할때, 다음과 같이 타입(T
)과 개체(val
)를 같이 전달받았는데요,(템플릿 인자 참고)
1
2
3
4
5
6
7
8
template<typename T, T val> // 타입과 개체를 전달받습니다.
class A {
public:
T GetVal() {return val;}
};
A<int, 10> a{};
EXPECT_TRUE(a.GetVal() == 10);
C++17 부터는 auto를 이용하여 임의 타입 개체를 전달받을 수 있습니다.
1
2
3
4
5
6
7
8
template<auto val> // 비타입 템플릿 인자인 경우 auto를 사용할 수 있습니다.
class A_17 {
public:
auto GetVal() const {return val;}
};
A_17<10> a_17{};
EXPECT_TRUE(a_17.GetVal() == 10);
(C++20~) 비타입 템플릿 인자 규칙이 완화되어 실수 타입과 리터럴 타입을 사용할 수 있습니다.
(C++20~) 축약된 함수 템플릿
기존에는 함수 인자로 auto를 사용할 수 없었고, C++14 부터 일반화된 람다 표현식에서만 auto를 사용할 수 있었습니다.(함수 인자, auto, 일반화된 람다 표현식 참고)
C++20 부터는 축약된 함수 템플릿이 추가되어 함수 인자로 auto를 사용할 수 있습니다. 사실상 함수 템플릿의 간략한 표현입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// template<typename T>
// void f(T param);
void f_20(auto param) {}
f_20(10);
// template<typename T, typename U>
// void g(T param1, U param2);
void g_20(auto param1, auto param2) {} // param1과 param2는 동일한 타입이라는 보장이 없습니다.
g_20(10, 3.14);
// template<typename... Params>
// void h_11(Params... params);
void h_20(auto... params) {} // 가변 템플릿도 지원합니다.
h_20(10, 3.14, "Hello");
// template<typename T, typename U>
// void i(T param1, U param2) {}
template<typename T>
void i_20(T param1, auto param2) {} // 템플릿 내에서 혼합해서 사용할 수 있습니다.
i_20(10, 3.14);
auto나 decltype(auto)와 가변 인자를 함께 사용할 수도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
int Sum_20(auto count, ...) { // 가변 인자를 사용하는 함수입니다.
int result = 0;
std::va_list paramList; // 가변 인자
va_start(paramList, count); // 가변 인자 처리 시작
for (int i = 0; i < count; ++i) {
result += va_arg(paramList, int); // 가변 인자 추출
}
va_end(paramList); // 가변 인자 처리 끝
return result;
}
EXPECT_TRUE(Sum_20(3, 1, 2, 3) == 1 + 2 + 3);
(C++20~) 비타입 템플릿 인자 규칙 완화
기존에는 비타입 템플릿 인자를 사용할 경우, 정수 타입, 열거형 상수의 열거자, 전역 또는 정적 개체의 포인터/참조자를 사용할 수 있었는데요(템플릿 인자 참고),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<int val>
class A {};
A<10> a; // 정수 타입
enum MyEnum{Val};
template<enum MyEnum myEnum>
class B {};
B<Val> b; // 열거형의 열거자
class MyClass {};
template<MyClass* ptr>
class C {};
template<MyClass& ref>
class D {};
MyClass g_MyClass;
C<&g_MyClass> c; // 전역, 정적 개체의 포인터
D<g_MyClass> d; // 전역, 정적 개체의 참조자
C++20 부터는 비타입 템플릿 인자 규칙이 완화되어
- 실수 타입과
- 모든 멤버 변수가
public
이고, mutable이 아니고, constexpr 생성자가 있는 리터럴 타입을
사용할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
template<double d>
class A_20 {};
A_20<3.14> a; // 실수 타입
class MyClass_11 {
public:
int m_Val; // 멤버 변수는 public이고, mutable이 아니어야 합니다.
public:
constexpr explicit MyClass_11(int val) : m_Val(val) {} // constexpr 생성자여야 합니다.
};
template<MyClass_11 myClass>
class B_20 {};
B_20<MyClass_11{1}> b; // 멤버 변수는 public이고, mutable이 없으며, constexpr 생성자가 있는 리터럴 타입
(C++20~) 클래스 템플릿 인수 추론시 initializer_list 개선
C++17 부터 클래스 템플릿 인수 추론이 추가(클래스 템플릿 인수 추론 참고) 되었으나, initializer_list로 초기화시에는 클래스 템플릿 인수 추론이 되지 않았습니다. 따라서, std::vector<int> v_11{1, 2, 3};
와 같이 템플릿 인자를 명시해야 했는데요,
C++20 부터는 클래스 템플릿 인수 추론시 initializer_list인 경우가 개선되어 std::vector v_20{1, 2, 3};
처럼 템플릿 인자를 명시하지 않아도 됩니다.
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class A {
public:
explicit A(T val) {}
};
A<int> a{10}; // ~C++17 이전에는 타입을 명시해야 합니다.
A a_17{10}; // C++17 부터는 인수 타입으로부터 추론합니다.
std::vector<int> v_11{1, 2, 3}; // ~C++20 이전에는 initializer_list로 초기화시에 타입을 명시해야 합니다.
std::vector v_20{1, 2, 3}; // C++20 부터는 initializer_list로 초기화시에 타입을 명시하지 않아도 됩니다.
댓글남기기