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의 종류에 따라 세 종류로 나뉜다.

  1. ParamType이 pointer 혹은 reference이지만 universal reference는 아닌 경우
  2. ParamType이 universal reference인 경우
  3. ParamType이 pointer도 아니고 reference도 아닌 경우

T&&은 일반적으로 rvalue reference이지만 타입을 추론해야하는 상황에서는 lvalue reference와 rvalue reference 중 하나라는 뜻인 universal reference로 사용된다.

Case 1. ParamType이 pointer 혹은 reference이지만 universal reference는 아닌 경우

위의 경우 다음과 같은 타입 추론이 이루어진다.

  1. expr의 참조성(reference-ness)은 무시한다.
  2. 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인 경우

위의 경우 다음과 같은 타입 추론이 이루어진다.

  1. 만약 expr이 lvalue이면, T와 ParamType 둘 다 lvalue reference로 추론된다.
  2. 만약 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 방식으로 다음과 같은 타입 추론이 이루어진다.

  1. expr의 참조성(reference-ness)은 무시한다.
  2. 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* constconst 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 (&)()


카테고리:

업데이트: