[EMC++] Item 1. template의 타입 추론 규칙을 숙지해라
Item 1. 템플릿 타입 추론 규칙을 숙지해라
우리는 template
을 통해 여러 자료형으로 사용 가능한 코드를 작성할 수 있다.
template
을 인스턴스화(instantiation)하는 과정에서 컴파일러가 타입을 추론하기 위해 타입 추론 규칙이 존재한다.
C++98에는 타입 추론 규칙이 template
밖에 없었지만 C++11부터는 타입 추론이 필요한 auto
, decltype
키워드가 추가되며 타입 추론 규칙 또한 추가되었다.
각 타입 추론 규칙에 대해 알아보자.
템플릿 타입 추론 규칙
template
은 함수나 클래스를 개별적으로 다시 작성하지 않아도, 여러 자료형으로 사용할 수 있도록 만들어 놓은 틀이다.
template
은 함수 템플릿(Function Template)과 클래스 템플릿(Class Template)으로 구현된다.
아래 코드는 함수 템플릿 예시이다.
template <typename T>
void f(ParamType param);
f(expr);
컴파일러는 컴파일 도중 expr를 이용해서 T와 ParamType에 대한 타입을 추론한다.
ParamType은 const
키워드나 &
, &&
같은 참조 한정사가 붙어서 T와 다르게 추론되는 경우가 많다.
T에 대한 타입 추론은 expr과 ParamType의 종류에 따라 세 종류로 나뉜다.
- ParamType이 pointer 혹은 reference이지만 universal reference는 아닌 경우
- ParamType이 universal reference인 경우
- ParamType이 pointer도 아니고 reference도 아닌 경우
T&&
은 일반적으로 rvalue reference이지만 타입을 추론해야하는 상황에서는 lvalue reference와 rvalue reference 중 하나라는 뜻인 universal reference로 사용된다.
Case 1. ParamType이 pointer 혹은 reference이지만 universal reference는 아닌 경우
위의 경우 다음과 같은 타입 추론이 이루어진다.
- expr의 참조성(reference-ness)은 무시한다.
- T의 타입을 결정하기 위해 expr의 타입을 ParamType과 패턴 매칭(Patten Matching)한다.
template <typename T>
void f(T& param);
int x = 27; // x의 타입은 int
const int cx = x; // cx의 타입은 const int
const int& rx = x; // rx의 타입은 const int&
f(x); // T는 int, ParamType은 int&
f(cx); // T는 const int, ParamType은 const int&
f(rx); // T는 const int, ParamType은 const int&
타입 추론에서 expr의 참조성(reference-ness)은 무시하지만 상수성(const-ness)은 유지한다.
template <typename T>
void f(const T& param);
int x = 27; // x의 타입은 int
const int cx = x; // cx의 타입은 const int
const int& rx = x; // rx의 타입은 const int&
f(x); // T는 int, ParamType은 const int&
f(cx); // T는 int, ParamType은 const int&
f(rx); // T는 int, ParamType은 const int&
다만 expr의 const
는 ParamType에 const
가 없으면 T 타입 추론에 포함하고 ParamType에 const
가 있으면 이미 const-ness가 보장되기 때문에 T 타입 추론에 포함하지 않는다.
pointer 또한 reference와 타입 추론 규칙이 같다.
Case 2. ParamType이 universal reference인 경우
위의 경우 다음과 같은 타입 추론이 이루어진다.
- 만약 expr이 lvalue이면, T와 ParamType 둘 다 lvalue reference로 추론된다.
- 만약 expr이 rvalue이면, Case 1의 규칙이 적용된다. (T 타입을 추론할 때 expr의 reference는 무시하고 const는 유지함)
템플릿 타입 추론에서 T가 reference로 추론되는 경우는 1번이 유일하다.
universal reference는 앞에
const
가 붙으면 rvalue reference가 된다.
template <typename T>
void f(T&& param);
int x = 27; // x의 타입은 int
const int cx = x; // cx의 타입은 const int
const int& rx = x; // rx의 타입은 const int&
f(x); // T는 int&, ParamType은 int&
f(cx); // T는 const int&, ParamType은 const int&
f(rx); // T는 const int&, ParamType은 const int&
f(27); // T는 int, ParamType은 int&&
Case 3. ParamType이 pointer도 아니고 reference도 아닌 경우
위의 경우 argument 값을 함수의 parameter로 복사하는 call by value 방식으로 다음과 같은 타입 추론이 이루어진다.
- expr의 참조성(reference-ness)은 무시한다.
- expr의 상수성(const-ness)를 무시한다.
template <typename T>
void f(T param);
int x = 27; // x의 타입은 int
const int cx = x; // cx의 타입은 const int
const int& rx = x; // rx의 타입은 const int&
f(x); // T, ParamType은 int
f(cx); // T, ParamType은 int
f(rx); // T, ParamType은 int
이는 argument의 원본이 아닌 복사된 값이기 때문에 당연한 결과이다. 원본이 const
라고 복사된 값이 const
일 필요는 없기 때문이다.
pointer의 경우 pointer가 가리키는 타입의 const-ness는 보존할 수 있지만 pointer 자체의 const-ness는 사라진다.
template <typename T>
void f(T param);
const char* ptr1 = "hello";
const char* const ptr2 = "hello";
f(ptr1); // T, ParamType은 const char*
f(ptr2); // T, ParamType은 const char*
const char* const
은 const char
타입을 가리키는 const
포인터로 주소값 변경이 불가능하지만 템플릿에 의해 추론되는 const char*
는 const char
타입을 가리키는 포인터로 주소값 변경이 가능하다.
즉, 타입 추론 과정에서 pointer의 const-ness가 사려졌다.
인자가 배열이나 함수일 경우
배열과 포인터는 다른 타입이지만 배열을 배열의 첫 원소를 가리키는 포인터를 붕괴(decay)해서 사용할 수 있는 Array Decay 때문에 타입을 혼동할 수 있다.
const char name[] = "heesu" // name 타입은 const char[6]
const auto ptrToName = name; // ptrToName 타입은 const char*
name과 ptrToName은 다른 타입이지만 Array Decay 덕분에 오류 없이 컴파일된다.
또한, 템플릿 타입 추론에서 배열 타입의 매개변수는 없기 때문에 포인터로 추론된다.
template <typename T>
void f(T param);
f(name) // T, ParamType은 const char*
void f(int param[]); // 실제로는 아래와 같은 함수
void f(int* param);
만약 ParamType이 reference인 경우, 배열로 추론이 가능하다.
template <typename T>
void f(T& param);
f(name); // T는 const char[6], ParamType은 const char (&)[6]
함수 포인터도 배열과 매우 비슷하다.
template <typename T>
void f1(T params);
template <typename T>
void f2(T& params);
template <typename T>
void f3(T&& params);
void f();
f1(f); // T는 void (*)(), ParamType은 void (*)()
f2(f); // T는 void(), ParamType은 void (&)()
f3(f); // T는 void (&)(), ParamType은 void (&)()