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에 의해서도 영향을 받는다.
그 형태에 따라 세 가지 케이스로 나뉘는데,
- ParamType이 포인터 또는 reference 타입이지만 universal reference는 아닌 경우
- ParamType이 universal reference일 경우
- ParamType이 포인터도 아니고 참조도 아닌 경우
template<typename T> void f(ParamType param); f(expr);
case 1 : ParamType이 포인터 또는 reference지만, universal reference는 아님
가장 간단한 케이스.
- 만약 expr이 reference이면 reference 부분을 무시한다.
- 그 다음 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이 새로 만들어지는 복사 객체라는 사실 때문에, 다음 규칙이 적용된다.
- case 1처럼, expr이 reference 타입이면 reference부분은 무시한다.(호출이 되긴 되나보다)
- 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 |
---|