템플릿 타입 추론은 C++11의 주요 feature 중 하나인 auto 타입 추론의 기반이기 때문에 잘 이해해야 한다.

 

function template은 다음과 같이 선언 및 호출할 수 있다.

// Pseudo Code

// Declaration
template<typename T>
void f(ParamType param);

// Function call
f(expr);

이때, expr을 통해서 TParamType을 추론한다.  T와 ParamType은 같을 수도 있지만 다른 경우도 존재한다. 예를 들어,

 

// Declaration
template<typename T>
void f(const T& param);

// Fucntion call
int x = 0;
f(x);

위의 경우, ParamType은 const int&이고, T는 int이다.

 

T와 ParamType을 추론하기 위해서는 ParamType이 어떻게 생겼는지에 따라 3가지로 경우를 나누어 생각해야 한다.

 

  • Case 1: ParamType이 pointer나 reference 타입인 경우(universal reference인 경우는 제외).
  • Case 2: ParamType이 universal reference인 경우.
  • Case 3: ParamType이 pointer도, reference도 아닌 경우.

위에서 universal reference란 용어가 나오는데... 일단 그냥 그런게 있다 정도로 넘어가도 무방하다. 나중에 [항목 24]에서 나올 예정이라고 하니 조금만 기다려보자.

 

이제 3가지 Case에 대해 하나씩 살펴보자!

 


Case 1: ParamType이 pointer나 reference 타입인 경우(universal reference인 경우는 제외).

 

이 경우는, 다음과 같이 추론을 할 수 있다.

 

  1. 만약 expr의 타입이 reference인 경우, reference를 무시한다.
  2. expr의 타입을 ParamType에 대해서 패턴 매칭하고, T를 추론한다.

 

일단 1번은 expr, 즉 호출부의 reference를 무시한다는 얘기니 간단한데, 2번은 아직 감이 안올 수 있다.

중요한 것은.. T는 ParamType이 정해지면 저절로 추론이 된다는 것이다.

예를 들어, ParamType이 T&라 하자. 패턴 매칭을 통해 ParamType이 const int&로 정해졌다면, 자연스럽게 T는 const int로 추론된다는 것을 알 수 있다.

 

그렇다면 패턴 매칭을 어떻게 한다는건지 첫 번째 예제를 통해 살펴보자.

 

// Declaration
template<typename T>
void f(T& param);	// ParamType이 reference이다.

// Fucntion call
int x = 27;
const int cx = x;
const int& rx = x;

f(x);
f(cx);
f(rx);

ParamType이 reference인 경우다(호출부의 reference를 무시하는 것이지, ParamType의 reference를 무시하는게 아님에 주의하자!).

이때 3가지 expr이 나오는데,

x는 그냥 int,

cx는 const int,

rx는 const int&이다.

 

이 3가지 경우에 대해 타입 추론을 해보자.

 

1. x: int

추론 과정의 첫 번째는 호출부의 reference 무시인데, x는 int이므로 애초에 reference가 아니다. 따라서 바로 패턴 매칭으로 넘어가자.

 

// expr -> ParamType

int -> T&

 

ParamType이 int&가 될 수 밖에 없다는것을 쉽게 알 수 있다!(char&가 되진 않을거니까..)

따라서 ParamType은 int&이고, 저절로 T는 int가 된다.

 

2. cx: const int

cx 역시 reference가 아니므로 바로 패턴 매칭으로 넘어가보자.

 

// expr -> ParamType

const int -> T&

 

ParamType은 이때 const int&가 된다. 호출부의 const가 T로 그대로 넘어온 것이다! 이부분이 좀 헷갈릴 수도 있을 것 같은데, 호출부의 특성을 다 포함하는 방향으로 T가 ParamType이 정해진다고 보면 될 것 같다.

 

ParamType이 const int&이므로, 저절로 T는 const int가 된다.

 

3. rx: const int&

드디어 추론 과정의 첫 번째 과정이 쓰이는 경우이다. 호출부가 reference이므로, reference를 무시한다. 그러면 const int가 되므로, 위의 cx 추론 과정과 동일해진다. 따라서 T는 const int가 된다.


이제 두 번째 예제로 가보자. 다 동일한데, ParamType이 T&에서 const T&로 바뀌었다.

// Declaration
template<typename T>
void f(const T& param);	// ParamType이 const reference이다.

// Function call
int x = 27;
const int cx = x;
const int& rx = x;

f(x);
f(cx);
f(rx);

ParamType이 const reference로 바뀌었을 때 어떻게 되는지 살펴보자.

 

1. x: int

호출부가 reference가 아니니 바로 패턴 매칭으로 넘어가자.

 

// expr -> ParamType

int -> const T&

 

ParamType은 const int&가 될 수 밖에 없어보인다. 따라서 T는 저절로 int가 된다.

 

2. cx: const int

역시 호출부가 reference가 아니니 바로 패턴 매칭으로 넘어가자.

 

// expr -> ParamType

const int -> const T&

 

호출부가 const지만 애초에 ParamType도 const이다. ParamType은 const int&가 되고, 따라서 T는 저절로 int가 된다.

 

3. rx: const int&

호출부의 reference를 무시하므로 const int가 되고, 위의 cx 추론 과정과 동일하므로 T는 int가 된다.

 

 

첫 번째 예제와 두 번째 예제의 차이점에 주목하자. x에 대해서는 똑같이 T가 int로 추론되었지만, cx와 rx에 대해서는 결과가 다르다.

 

첫 번째 예제는 ParamType이 T&였던 경우였다. 이때 cx와 rx에 대해서 ParamType이 const int&가 되었기 때문에 T는 const int로 추론되었다.

 

반면 두 번째 예제는 ParamType이 const T&였으므로, cx와 rx에 대해서 ParamType이 const int&가 되어도 T는 그냥 int로 추론되었다.

 

ParamType에 const가 붙었냐에 따라 T가 int가 될 수도, const int가 될 수도 있다는 것을 알고 넘어가자!

 


Case 2: ParamType이 universal reference인 경우.

 

universal reference에 대해서 잘 몰라도, 여기서는 어떻게 추론되는지 핵심만 알고가자.

 

  1. 만약 expr가 lvalue라면, T와 ParamType은 둘 다 lvalue reference로 추론된다.
  2. 만약 expr가 rvalue라면, Case 1과 동일하다.

 

1번은 두 가지 측면에서 주목하고 넘어가야 한다.

첫 번째로, 템플릿 타입 추론에서 T가 reference로 추론되는 유일한 경우이다.

두 번째로, ParamType이 rvalue reference의 형태로 선언되었지만, 추론된 타입은 lvalue reference이다.

 

예제를 통해 어떻게 추론되는지 알아보자.

 

 

// Declaration
template<typename T>
void f(T&& param);	// ParamType이 universal reference이다.

// Function call
int x = 27;
const int cx = x;
const int& rx = x;

f(x);
f(cx);
f(rx);
f(27);​

1. x: int

x가 lvalue이므로, T와 ParamType 둘 다 int&로 추론된다.

 

2. cx: const int

cx가 lvalue이므로, T와 ParamType이 둘 다 const int&로 추론된다. 호출부의 const가 넘어오는 것은 Case 1과 동일하다.

 

3. rx: const int&

rx 역시 lvalue이므로, T와 ParamType이 둘 다 const int&로 추론된다.

 

4. 27: rvalue int

27은 rvalue이다. 따라서 Case 1과 동일하게 추론을 한다.

 

// expr -> ParamType

int -> T&&

 

ParamType은 int&&가 되고, 따라서 T는 저절로 int가 된다.

 

 

universal reference인 경우는 호출부가 lvalue인지, rvalue인지에 따라 다르게 추론된다는 것에 주목을 해야 한다. Case 1에서는 lvalue, rvalue 모두 동일한 규칙에 의해 추론되었었다. T가 reference로 추론되는 유일한 경우임을 알아두는 것도 중요해보이는 포인트이다!

 


Case 3: ParamType이 pointer도, reference도 아닌 경우.

 

즉, pass-by-value인 경우이다. 이 경우 규칙은 다음과 같다.

 

  1. 만약 expr의 타입이 reference인 경우, reference를 무시한다.
  2. 만약 expr의 reference를 무시했는데 expr가 const나 volatile인 경우, const와 volatile을 무시한다.

 

(volatile에 대해서는 [항목 40]에 나온다.)

 

사실 규칙이 저렇긴 한데, 결국 pass-by-value의 본질에 대해 생각해본다면 당연하게 느껴질 수 있다. 어차피 복사본이 들어가는데, 원본이 reference인지 const인지 알게 뭔감?

 

첫 번째 예제를 통해 살펴보자.

// Declaration
template<typename T>
void f(T param);	// pass-by-value이다.

// Function call
int x = 27;
const int cx = x;
const int& rx = x;

f(x);
f(cx);
f(rx);

x, cx, rx 모두에 대해 T와 ParamType은 모두 int이다.

 

함수의 argument, 즉 호출부가 const든 reference든, parameter로 넘어온 시점에서는 복사본이기 때문에 다 무시된다.

 

간단해 보이지만, 두 번째 예제를 통해 주의할 점을 살펴보자.

 

// Declaration
template<typename T>
void f(T param);	// ParamType이 universal reference이다.

// Function call
const char* const ptr =
  "Fun with pointers";
  
f(ptr);

어차피 pass-by-value니까 const 다 무시하면,, ParamType, T 둘 다 char*겠지? 하면..틀리다!

 

우선 'const char* const ptr' 에서, char* 왼쪽의 const 의미와 오른쪽의 const 의미를 알고 있어야 한다.

 

왼쪽의 const는 ptr이 가리키는 character string이 const란 의미이고, 오른쪽의 const는 ptr 자체가 const라는 의미이다.

즉.. 우리가 보통 생각하는 const의 이미는 오른쪽의 const를 의미한다. ptr = "Another char"; 이렇게 하면 오류를 발생시키는 부분은 오른쪽의 const이다.

 

const char* ptr; 라고 하면, ptr의 타입 자체가 const char*라고 생각하는게 편하다. 즉 왼쪽의 const는 타입의 일부분인 것이다.

 

아무튼, 규칙에 의해서 무시되는 const는 오른쪽의 const이다. 따라서 T와 ParamType은 const char*로 추론된다.

 

 

+ Recent posts