Item 1: Understand template type deduction.

템플릿 타입 추론에 대하여 알아보자.


c++ 11에서 컴파일러의 auto에 대한 타입 추론은 템플릿에 대한 타입 추론을 기반으로 작동한다.

단, 그러한 규칙들이 auto의 context의 적용될 때에는 템플릿의 경우에 비해 덜 직관적인 경우가 있다.

따라서, auto를 잘 활용하려면 auto가 대체 어떻게 돌아가는 놈인지 확실히 이해하고 있어야 한다.


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

함수 템플릿의 선언은 대충 위와 같이 쓴다.


그리고 호출하는 곳은 대체로

f(expr);//어떤 표현식으로 f를 호출

이런 느낌..


컴파일 타임에 컴파일러는 expr을 통해 두 가지의 타입을 추론한다.

하나는 타입 T, 하나는 ParamType의 타입이다.

(왜 따로냐 하면, ParamType이 대개 const T& 등등 수식어들이 붙기 때문)


예를 들어,

template<typename T>
coid f(const T& param);

int x = 0;
f(x);

위에서 T는 int로 추론되고, ParamType은 const int&로 추론된다.(당연)


T가 expr과 같은 타입일 것이라고 기대하는 것은 당연하나, T의 타입 추론은 expr뿐만 아니라 ParamType에 의해서도 영향을 받는다.

그 형태에 따라 세 가지 케이스로 나뉘는데,

  1. ParamType이 포인터 또는 reference 타입이지만 universal reference는 아닌 경우
  2. ParamType이 universal reference일 경우
  3. ParamType이 포인터도 아니고 참조도 아닌 경우
이렇게 세 가지이다.

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

f(expr);

case 1 : ParamType이 포인터 또는 reference지만, universal reference는 아님

가장 간단한 케이스.

  1. 만약 expr이 reference이면 reference 부분을 무시한다.
  2. 그 다음 expr의 타입을 Paramtype에 대해 pattern-matching 방식으로 대응시켜 T의 타입을 결정한다.
결과는 - 
template<typename T>
void f(T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x); //T는 int, param은 int&
f(cx);//T는 const int, param은 const int&
f(rx);//T는 const int, param은 const int&

f의 매개변수 타입을 const T&로 바꾸면 객체에 대한 const성의 기대 충족을 param에서 채워줄 수 있으므로, const가 T한테까지 가지 않아도 된다.

결과는 - 

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

int x = 27;
const int cx = x;
const int& rx = x;

f(x); //T는 int, param은 const int&
f(cx);//T는 int, param은 const int&
f(rx);//T는 int, param은 const int&

param이 reference가 아니라 포인터라도 타입 추론은 똑같이 돌아간다.

결과는 - 

template<typename T>
void f(T* param);

int x = 27;
const int* px = &x;

f(&x);//T는 int, param은 int*
f(px);//T는 const int, param은 const int*


case 2 : ParamType이 universal reference임

universal reference가 뭔지 나는 모른다. (항목24에 나온다고 함)

일단은 대충 이렇다고 한다.

  • expr이 L-value이면, T와 ParamType 둘 다 L-value reference로 추론됨.
    • 웃긴점 1 : 템플릿 타입 추론에서 T가 reference로 추론되는 경우는 이 case가 유일.
    • 웃긴점 2 : ParamType은 겉으로 보기에 R-value reference일 것 처럼 생겼지만, 추론되는 결과 타입은 L-value이다.
  • expr이 R-value이면, case 1을 따름
무슨 소리인지 모르겠지만, 예시를 보자.
template<typename T>
void f(T&& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x);//x는 lvalue, T는int&, param도 int&
f(cx);//cx는 lvalue, T는 const int&, param도 const int&
f(rx);//rx는 lvalue, T는 const int&, param도 const int&
f(27);//27은 rvalue, T는 int, param은 int&&

이유는 잘 모르겠으나, 일단 외우기로 하였다.

왜 이렇게 되는지에 대한 이뉴는 item 24에 나온다고 하니 일단은 외우자.


case 3 : ParamType이 포인터도 아니고 reference도 아님

그냥 byVal로 전달되는 케이스이다.

param이 새로 만들어지는 복사 객체라는 사실 때문에, 다음 규칙이 적용된다.

  1. case 1처럼, expr이 reference 타입이면 reference부분은 무시한다.(호출이 되긴 되나보다)
  2. expr의 참조성을 무시한 후, expr에 const성이 있으면 그 const성 역시 무시한다.
    만약 volatile이면 그것도 무시한다.(엥??왜지?? - 항목40에 나온다고 함)

예시를 보자.

template<typename T>
void f(T param);

int x = 27;
const int cx = x;
const int& rx = x;

//다음 세 가지 case 모두 T와 param 둘 다 int
f(x);
f(cx);
f(rx);


보고 가야 할 예시도 있다.

template<typename T>
void f(T param);//값복사

//const char에 대한 const형 포인터
const char* const ptr = "Fun with pointers";

f(ptr)//expr은 const char* const이다.

위 예시에서 expr은 const char* const이고, const형 char에 대한 const형 "포인터"이다.

결국 메모리 주소(포인터의 값)를 전달하게 되고, 그에 대한 const는 무시된다.

하지만, 가리키고 있던 대상(const char)의 const성은 포인터의 값복사 여부와는 아무 상관이 없으므로, param은 const char*이 된다.


모든 case에 대하여 알아본 것 같지만, 틈새 케이스가 있다.

그것은 바로바로 배열타입..(포인터 타입과 같은 게 아니었다?!)

배열과 포인터를 맞바꿔 쓸 수 있는 것처럼 보이게 하는 주 원인은, 많은 경우에서 배열타입이 배열의 첫 원소를 가리키는 포인터로 붕괴(decay)한다는 점이다.

const char name[] = "Gogiga meokgo sipda";//name은 const char*이 아니라 const char[13]이다.

const char * ptrToName = name;//배열이 포인터로 붕괴된다.

전혀 몰랐던 사실이다. 배열->포인터 붕괴 규칙때문에 오류 없이 잘 컴파일된다.

하지만 byVal로 매개변수를 받는 템플릿에 배열을 넣으면 어떻게 될까?

//(위 코드에 이어서)
template<typename T>
void f(T param);

f(name);//과연??

우선, 배열 타입의 매개변수라는 것은 없다는 점부터 짚고 넘어가자.(배열 타입이라는 게 있는지도 몰랐다.

void some_func(int param[]);

그런데도 위 구문이 적법한 이유는, 이 경우 배열 선언이 하나의 포인터 선언으로 취급되기 때문.(int* param과 완전히 같은 의미이다.)

위 케이스와 같이, 배열타입을 byVal로 전달하는 템플릿 함수에 매개변수로 넣으면, 배열 타입이 포인터형으로 추론된다.

즉, name은 배열이지만, T는 const char*가 되는 것이다.

하지만 여기서 신기한 요령이 있는데, 배열 타입 매개변수는 불가능하지만, 배열 타입에 대한 참조 타입 매개변수는 가능하다는 것이다.

따라서,

template<typename T> void f(T& param); const char name[] = "123..."; f(name);

요렇게는 가능하다는 것이다. 그러면 T는 배열의 실제 타입이 된다(!!!).

배열 타입은 배열의 크기를 포함하므로, 위 케이스에서 T는 const char[7]로 추론되고, param은 const char (&)[7]이 될 것이다.(해괴한 문법...)

이를 활용하면 신기하게도 배열의 원소 개수를 추론하는 템플릿을 만들 수 있다.

//배열의 크기를 컴파일 시점 상수로 리턴하는 템플릿 함수
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
    return N;
}

constexpr로 선언하면 함수호출의 결과를 컴파일타임에 사용할 수 있게 된다고 한다(item 15에서 다시 설명).

문법이 낯설고 해괴하지만, 어떻게 돌아가는 건지 꼼꼼히 확인해 볼 필요가 있겠다.


위에서 설명한 배열에 대한 추론에 관련한 모든 것이 함수 타입에 대하여도 똑같이 적용된다.

똑같은 원리에 의해서 함수 타입이 함수 포인터로 붕괴 가능하다.(함수 타입이 있었다니..)

실제 응용에서 신경 쓸 부분은 아니라고 하나, 알아둬서 나쁠 것은 없겠다.



Things to Remember

  • 템플릿 타입추론 도중에 reference 타입의 인자들은 const성이 무시된다.
  • universal reference에 대한 타입추론에서 L-value들은 특별하게 취급된다.
  • byVal 방식의 매개변수에 대한 타입추론에서 const와 volitile은 무시된다.
  • 템플릿 타입추론 과정에서 배열(함수)형식의 인수는 포인터로 붕괴한다.
    단, 배열(함수)에 대한 reference일 경우에는 붕괴하지 않는다.


'C++ > Effective Modern C++' 카테고리의 다른 글

Item 2: Understand auto type deduction.  (0) 2016.03.24
Posted by RPG만들기XP
,