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가 레퍼런스라면 해당 값에 대한 레퍼런스로 받는다.
댓글 없음:
댓글 쓰기