클래스 템플릿
클래스 템플릿은 클래스를 찍어내는 도구입니다.
여기서 말하는 클래스는 넓은 의미의 집합체를 의미하고, C++에서 이러한 집합체에는 class, struct 그리고 union이 있습니다.
이 클래스 템플릿을 사용하는 이유는 함수 템플릿을 사용하는 이유와 같고, 내용을 설명하는 데 사용되는 용어도 동일합니다.
차이라면, 함수 템플릿은 함수를, 클래스 템플릿은 클래스를 생성한다는 것뿐일 겁니다.
그러므로, 함수 템플릿을 설명한 글을 아직 보지 못했거나, 기억이 흐릿해졌다면 먼저 읽어보길 추천합니다.
함수 템플릿은 원하는 타입의 매개 변수를 사용하는 함수를 만들어 내는 데 사용하는 틀( template )이라고 할 수 있습니다.
그리고, 이러한 함수 템플릿( function template )은 템플릿 매개변수 선언( template parameters declaration )과 함수 템플릿 정의로 이루어져 있습니다.
template< typename T > // 템플릿 매개변수 선언
T add( T a, T b ){ // 함수 템플릿 정의
return a + b;
}
이와 비슷하게, 클래스 템플릿( class template )은 원하는 타입의 멤버 변수와 그러한 타입의 매개변수를 가진 멤버 함수들로 구성된 클래스를 만들어 내는 틀입니다.
그리고, 이 클래스 템플릿은 템플릿 매개변수 선언( template parameters declaration )과 클래스 템플릿 정의로 이루어져 있습니다.
template < typename T > // 템플릿 매개변수 선언
struct Pair{ // 클래스 템플릿 정의
T first{};
T second{};
};
위의 예문은, 임의의 타입인 두 멤버 변수를 가진 클래스 템플릿을 보여줍니다.
이러한 클래스 템플릿에서 사용된 형식 템플릿 매개변수( type template parameter )인 T는 임의의 타입으로 대체될 자리를 표시합니다.
그리고, 컴파일러는 주어진 템플릿 인수( template argument )와 위와 같은 클래스 템플릿을 이용해서, 실제 클래스를 구체화( instantiation )하고, 이 클래스의 객체를 생성합니다.
int main(){
Pair< int > p1{ 3, 5 };
cout << "first: " << p1.first << ", second: " << p1.second << endl;
Pair< double > p2{ 1.5, 3.14 };
cout << "first: " << p2.first << ", second: " << p2.second << endl;
}
▼출력
first: 3, second: 5
first: 1.5, second: 3.14
위의 예문에서, 컴파일러가 Pair<int> 를 보게 된다면 템플릿 인수 <int> 를 사용해서 다음과 같은 클래스를 생성합니다.
template<> // 클래스 템플릿으로부터 구체화되었음을 알림
struct Pair<int>{
int first{};
int second{};
};
즉, 클래스 템플릿에 사용된 형식 템플릿 매개변수 T 를 int로 대체하는 것입니다.
그리고, 이 클래스 정의로부터 객체 p1 을 생성합니다.
두 번째 객체를 생성하는 과정도 동일합니다.
단지, 각각의 객체가 가지고 있는 멤버들의 타입만 다를 뿐입니다.
template<>
struct Pair<double>{
double first{};
double second{};
};
그리고, 컴파일러에 의해 구체화된 클래스들은 Pair<init> 와 Pair<double> 이름을 갖게 되기 때문에, 클래스의 정의 중복으로 인한 오류가 발생되지 않습니다.
만일 같은 타입의 템플릿 인수를 사용하면, 컴파일러는 클래스를 다시 생성하지 않고, 이미 구체화되어 있는 클래스를 재사용해서 객체를 생성하게 됩니다.
또한, 함수 템플릿과 마찬가지로, 클래스 템플릿도 여러 개의 형식 템플릿 매개 변수를 가질 수 있습니다.
아래의 예문은 서로 다른 타입의 멤버를 가질 수 있는 Pair 클래스 템플릿을 보여줍니다.
template < typename T, typename U > // 2개의 형식 템플릿 매개변수
struct Pair{
T first{};
U second{};
};
int main(){
Pair< int, double > p1{ 2, 3.14 };
cout << "first: " << p1.first << ", second: " << p1.second << endl;
Pair< int, int > p2{ 2, 5 };
cout << "first: " << p2.first << ", second: " << p2.second << endl;
}
클래스 템플릿 인수 추론( class template argument deduction, CTAD )
C++ 17 이상의 버전을 사용한다면, 위의 main 함수를 다음과 같이 작성할 수도 있습니다.
#include <iostream>
using namespace std;
template < typename T, typename U > // 클래스 템플릿
struct Pair{
T first{};
U second{};
};
int main(){
//Pair< int, double > p1{ 2, 3.14 }; C++ 17 이전의 방식
Pair p1{ 2, 3.14 };
cout << "first: " << p1.first << ", second: " << p1.second << endl;
//Pair< int, int > p2{ 2, 5 };
Pair p2{ 2, 5 };
cout << "first: " << p2.first << ", second: " << p2.second << endl;
}
위에서 주석으로 처리한 문장이 이전 항목에서 클래스 객체를 생성하기 위해 사용했던 방법입니다.
그리고, 바로 아래의 문장은 이 주석 문장에서 템플릿 인수( template argument )를 생략해서 간단하게 선언한 문장입니다.
이러한 문장이 가능한 것은, C++ 17 버전 이후에 컴파일러가 주어진 인수로부터 템플릿 인수를 추론할 수 있게 되었기 때문입니다.
이러한 기능을 클래스 템플릿 인수 추론( class template argument deduction, 줄여서 CTAD )이라고 합니다.
그런데, 위의 코드를 직접 컴파일해 보면 컴파일러는 오류를 발생시킵니다.
▼출력
error: class template argument deduction failed
사실, 이러한 방식을 사용하려면 한 가지 장치가 더 필요하기 때문입니다.
그 장치를 템플릿 인자 추론 가이드( template argument deduction guide )라고 합니다.
// C++ 17 버전에서 필요한 deduction guide for Pair
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
이 가이드는 컴파일러가 형식 템플릿 매개변수를 추론하는 방식에 대한 힌트를 줍니다.
예를 들어, 위의 예제에서 컴파일러가 Pair { 2, 3.14 } 라는 선언을 만나게 되면, Pair { int , double } 형태의 선언으로 추론합니다.
그리고, 위의 추론 가이드의 힌트에 따라 Pair<int, double> 형태의 템플릿 클래스를 구체화하게 되는 것입니다.
다행인 것은, C++ 17 이후의 버전에서는, 이러한 템플릿 인자 추론 가이드가 필요 없다는 것입니다.
C++ 20 이후의 컴파일러는 클래스 템플릿을 위한 가이드를 자동으로 작성합니다.
여기까지 글을 읽었다면, 아마 커다란 물음표가 떠오를 것입니다.
위의 Pair 클래스 템플릿과 비슷한 std::pair를 사용할 때는, 이와 같은 오류를 본 적이 없기 때문입니다.
int main(){
std::pair p1{ 2, 3.14 }; // ok
cout << "first: " << p1.first << ", second: " << p1.second << endl;
std::pair p2{ 2, 5 }; // ok
cout << "first: " << p2.first << ", second: " << p2.second << endl;
}
하지만, 이것은 이러한 제약 조건이 있다는 것을 인식하지 못하고, std::pair 템플릿을 사용해 왔을 뿐입니다.
이 클래스 템플릿의 정의가 선언되어 있는 헤더파일을 찾아보면, 다음과 같은 문장을 찾아볼 수 있습니다.
// std::pair에서 발췌
template<typename _T1, typename _T2>
pair(_T1, _T2) -> pair<_T1, _T2>;
정리하자면, C++ 17 버전에서 컴파일러를 통한 클래스 템플릿 인수 추론 방법을 사용하려면, 클래스 템플릿 인수 추론 가이드가 필요하다는 것입니다.
template < typename T, typename U > // 클래스 템플릿
struct Pair{
T first{};
U second{};
};
// C++ 17에서만 필요한 추론 가이드
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
int main(){
Pair p1{ 2, 3.14 }; // 클래스 템플릿 인수 추론
//...
}
이러한 CTAD에 익숙해지면, 다음 예문과 같이 편리함을 느낄 수 있습니다.
#include <array>
int main(){
std::array<int, 5> arr1{ 1, 2, 3, 4, 5}; // 일반적인 선언 방법
std::array arr2{ 1, 2, 3, 4, 5}; // C++ 17 이후에만 가능
}
main의 마지막 문장이 CTAD를 통한 클래스 객체를 선언하는 방법을 보여줍니다.
형식 템플릿 매개변수의 기본값
함수의 매개변수가 기본 값을 가질 수 있는 것과 마찬가지로, 형식 템플릿 매개변수도 기본 타입을 지정할 수 있습니다.
이 기본 타입은 템플릿 인수가 지정되지 않고, 템플릿 매개변수를 추론할 수도 없는 경우에 사용됩니다.
// 기본값을 가진 형식 템플릿 매개변수
template < typename T = int, typename U = double >
struct Pair{
T first{};
U second{};
};
// 클래스 템플릿 인수 추론 가이드 for Pair
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
int main(){
// 형식 템플릿 매개변수를 지정한 경우
Pair< double, char > p1{ 3.14, 'c'};
Pair p2{ 2, 3.15}; // CTAD
Pair p3; // 기본 타입의 Pair<int, double>
}
main의 맨 마지막 문장처럼, 템플릿 인수를 지정하지 않고, 초기값이 없어서 템플릿 매개변수를 추론할 수도 없는 경우, 형식 템플릿 매개변수에 지정된 기본값으로 템플릿 클래스를 구체화하게 됩니다.
만약, 이 기본 값도 설정하지 않으면, 당연히 오류가 발생됩니다.
이 글과 관련있는 글들
클래스 템플릿의 특수화( class template specialization )
'C, C++ > C++ 언어' 카테고리의 다른 글
[C++] 함수 템플릿의 특수화( function template specialization ) (0) | 2024.10.18 |
---|---|
[C++] 비-형식 템플릿 매개변수( non-type template parameter) (0) | 2024.10.18 |
[C++] 함수 템플릿( function template )에 대한 설명 2 (0) | 2024.10.13 |
[C++] 함수 템플릿 ( function template )에 대한 설명 1 (1) | 2024.10.13 |
[C++] 포인터 this의 이해 (2) | 2024.10.07 |