2016년 3월 23일 수요일

Effective Modern C++, Item1: 템플릿 타입 추론을 이해하자

c++에서는 템플릿 메소드를 제공한다. 템플릿을 사용하면 컴파일러는 해당 변수의 타입을 추론하는데, 이 추론이 생각과 다르게 동작할 수 있다. 따라서 자세히 어떻게 되는지 알아야 한다.

template<typename T>

void f(ParamType param);



f(expr);


기본적으로 위의 T는 expr의 타입에 따라서 결정되지만 또한 ParamType에도 영향을 받는다. ParamType에 따라 영향을 받는 경우는 3가지로 분류할 수 있다.


케이스 1 : ParamType이 레퍼런스 포인트이고, 유니버셜 레퍼런스가 아닐 때.

가장 간단한 상황으로, ParamType이 레퍼런스 타입이거나 포인터 타입이면서 유니버셜 레퍼런스가 아닐 때 적용된다.

1. 만약 expr의 타입이 레퍼런스라면 레퍼런스 부분을 무시한다.
2. 그리고 expr의 타입과 ParamType을 비교하여 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&가 된다. 레퍼런스는 무시되기 때문.



케이스 2: ParamType이 유니버셜 레퍼런스 일 때

유니버셜 레퍼런스는 T&&와 같이 정의된다.
만약 expr이 lvalue일 때 T와 ParamType은 모두 lvalue 레퍼런스로 추론된다.
만약 expr이 rvalue라면, 케이스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는 lavalue이고 T는 const int&가 된다. param의 타입 또한 const int&이다.

f(rx); // rx는 lavalue이고 T는 const int&가 된다. param의 타입 또한 const int&이다.

f(27); // 27은 rvalue이고 T는 int가 된다. param의 타입은 int&&가 된다.



케이스 3: ParamType이 포인터나 레퍼런스가 아닐 때

ParamType이 포인터나 레퍼런스가 아니라면 값의 복사가 진행된다. param이 새로운 오브젝트로 생성된다는 것은 어떻게 T의 타입이 expr으로 부터 추론되는지 알 수 있게 해준다.

1. 이 전처럼 만약 exprt이 레퍼런스 타입이라면 레퍼런스 부분을 무시한다.
2. expr의 레퍼런스 타입을 무시한 뒤 expr이 const라면 그것 또한 무시한다. 만약 expr이 volatile이라면 이것 또한 무시한다.

따라서 다음 예제와 같은 결과를 확인할 수 있다.

template<typename T>

void f(T param);



int x = 27;

const int cx = x;

const int& rx = x;



f(x); // T와 param 모두 int가 된다.

f(cx); // T와 param 모두 int가 된다. const가 무시되기 때문.

f(rx); // T와 param 모두 int가 된다. 레퍼런스와 const를 무시하기 때문.


여기서 param은 전혀 새로운 오브젝트로 생성되기 때문에 const가 무시되는 것은 당연하게 이해할 수 있다.

아래와 같은 예제를 보자.

template<typename T>

void f(T param);



const char* const ptr = "Fun with pointers";



f(ptr);


위의 ptr의 의미는 ptr이 const 이기 때문에 ptr이 가르키고 있는 주소는 변경될 수 없고, char* 또한 const이기 때문에 char* 배열의 내용 또한 바뀔 수 없다는 것이다.

위와 같이 실행하였을 때 ptr의 값은 복사 될 것이고 ptr의 const는 무시될 것이다. 따라서 param의 타입의 추론은 const char* 가 될 것이다. ptr이 가르키는 대상의 const는 유지되는 것이다.


배열 인자

위의 세 가지 경우 외에도 추가로 알아두면 좋은 것들이 있다. 그 중하나가 배열이 인자로 전달되는 경우이다. 일반적으로 배열은 배열의 첫 번째 값을 가르키는 포인터로 decay 될 수 있다. decay 된다는 뜻은 배열과 배열의 첫번 째 인자를 가르키는 포인터를(비록 같지 않지만) 동일하게 취급하여 컴파일 시 문제가 없게 한다는 것이다.

const char name[] = "J. P. Briggs";



const char * ptrToName = name; // 이처럼 decay 된다고 이해할 수 있다.



하지만 배열이 template에서 by-value로 넘어가면 어떻게 될까?

template<typename T>

void f(T param); // 위에서 언급한 케이스 3에 해당된다.



f(name);



void myFunc(int param[]); // 이와 같은 표현은 적법하지만 배열 선언은 포인터와 같이 취급된다. 따라서 아래와 같다.



void myFunc(int* param);


이러한 문법은 c로부터 온 것이고 위의 경우 두 개의 함수는 동일하다. 이로 인해서 배열과 포인터가 같다고 생각하는 경우가 발생할 수 있다.

위의 경우 name이 포인터로 decay 되어 넘어가기 때문에 param의 타입은 const char*가 될 것이다.

하지만 만약 아래와 같이 받으면 어떻게 될까?

template<typename T>

void f(T& param);



f(name);


위의 경우 name이라는 배열에 대한 레퍼런스가 생성되어 param의 타입이 된다. 즉, const char[13] 그대로 name의 타입을 인식하고, param의 타입은 const char (&)[13] 이 된다.


함수 인자

c++ 에서 포인터로 decay되는 것은 배열만이 아니라 함수도 있다. 그리고 우리가 array에 대해서 이야기했던 모든 규칙들은 함수에도 적용이 된다.

void someFunc(int, double);



template<typename T>

void f1(T param);



template<typename T>

void f2(T& param);



f1(someFunc); // someFunc가 포인터로 decay 되어 param의 타입은 void (*)(int, double)이 된다.



f2(someFunc); // someFunc에 대한 레퍼런스로 받아 param의 타입은 void (&)(int, double)이 된다.



-------

핵심 요약

1. 템플릿 타입 추론 도중 레퍼런스인 인자들의 레퍼런스 성질은 무시된다.

2. 유니버셜 레퍼런스 인자를 추론할 때 lvalue는 특별 취급을 받는다.

3. by-value 인자를 추론할 때 const와 volatile은 그 성질을 잃는다.

4. 템플릿 타입 추론 도중 배열이나 함수인 인자는 포인터로 decay 된다. T가 레퍼런스라면 해당 값에 대한 레퍼런스로 받는다.


댓글 없음:

댓글 쓰기