Item1. Template Type의 추론 규칙을 숙지하라.
<span style="color:indianred">📘핵심 Note</span>
- 템플릿 형식 연역 도중 참조 형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
- 보편 참조 매개변수에 대한 형식 연역 과정에서 왼값 인수들은 특별하게 취급된다.
- 값 전달 방식의 매개변수에 대한 형식 연역 과정에서 const 또는 volatile(또는 그 둘 다인) 인수는 비 const, 비 volatile 인수로 취급된다.
- 템플릿 형식 연역 과정에서 배열이나 함수 이름에 해당하는 인수는 포인터로 붕괴한다. 단, 그런 인수가 참조를 초기화하는데 쓰이는 경우, 포인터로 붕괴하지 않는다.
템플릿 형식 연역 도중 참조 형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
함수 템플릿의 선언은 대체로 이런 모습이다.
template<typename T>
void f(ParamType param);
f(expr);
컴파일러는 컴파일 시expr를 이용해서 두 가지의 타입을 추론한다.
- T에 대한 타입 추론
- ParamType에 대한 타입 추론
이 두 타입은 서로 다른 경우가 많은데, 이는 ParamType에 흔히 const나 참조 한정사(&나 &&를 말한다) 같은 수식어들이 붙기 때문이다. 예를 들어 템플릿의 선언이 다음과 같다고 하자.
template<typename T>
void f(const T& param);
int x = 0;
f(x);이 경우 T는 int로 추론되나 ParamType은 const int&로 추론된다.
따라서 T에 대해 추론된 타입은 expr의 타입 뿐만 아니라 ParamType의 형태에도 의존한다. 그 형태에 따라 총 세 가지 경우로 나뉜다.
- ParamType이 포인터 또는 참조 형식이지만 보편 참조(universal reference) 는 아닌 경우
- ParamType이 보편 참조인 경우
- ParamType이 포인터도 아니고 참조도 아닌 경우
ParamType이 포인터 또는 참조 형식이지만 보편 참조(universal reference) 는 아닌 경우
이 경우, 형식 추론은 다음과 같이 진행된다.
1. 만일 expr 이 참조 형식이면 참조 부분을 무시 2. 그 다음 expr 의 형식을 ParamType 에 대해 패턴 부합(pattern-matching) 방식으로 대응시켜 T 의 형식을 결정
//-----------------------------------------------------------------------------
// 함수 템플릿이 다음과 같다고 하자.
template <typename T>
void f(T& param); // param은 참조 타입
// 그리고 이런 변수 선언들이 있다고 하자.
int x = 27; // x는 int
const int cx = x; // cx는 const int
const int& rx = x; // rx는 const int인 x에 대한 참조
// 다음은 여러 가지 호출에서 param과
// T에 대한 추론된 타입들이다.
f(x); // T는 int, param의 타입은 int&
f(cx); // T는 const int,
// param의 타입은 const int&
f(rx); // T는 const int,
// param의 타입은 const int&
// 둘째, 셋째 호출에서 cx와 rx에
// const 값이 대입되었기 때문에
// T는 const int로 추론된다.
// 셋째 호출에서, 비록 rx의 타입이
// 참조이지만 T는 비참조로 추론되었음을
// 주목하자.
//-----------------------------------------------------------------------------
// f의 매개변수의 타입을 T&에서
// const T&로 바꾸면 상황이
// 조금 달라진다.
template <typename T>
void f(const T& param); // 이제는 param이 const에 대한 참조
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&
// 이전처럼, 타입 추론 과정에서 rx의
// 참조성은 무시된다.
//-----------------------------------------------------------------------------
// param이 참조가 아니라 포인터
// (또는 const를 가리키는 포인터)
// 라도 타입 추론은 본질적으로 같은
// 방식으로 진행된다.
template <typename T>
void f(T* param); // 이번에는 param이 포인터
int x = 27; // 이전과 동일
const int *px = &x; // px는 const int로서의 x를 가리키는 포인터
f(&x); // T는 int, param의 타입은 int*
f(px); // T는 const int,
// param의 타입은 const int*ParamType이 보편 참조인 경우
만일 expr이 좌측값이면, T와 ParamType 둘 다 좌측값 참조로 추론된다. 이는 이중으로 비정상적인 상황이다.     첫째로, 템플릿 타입 추론에서 T가 참조 타입으로 추론되는 경우는 이것이 유일하다.     둘째로, ParamType의 선언 구문은 우측값 참조와 같은 모습이지만, 추론된 타입은 좌측값 참조이다. 만일 expr이 우측값이면, '정상적인'(즉, 경우 1의) 규칙들이 적용된다.
template<typename T>
void f(T&& param); // 이번에는 param이 보편 참조(universal reference)
int x = 27; // 이전과 동일
const int cx = x; // 이전과 동일
const int& rx = x; // 이전과 동일
f(x); // x는 좌측값, 따라서 T는 int&,
// param의 타입 역시 int&
f(cx); // cx는 좌측값, 따라서 T는 const int&,
// param의 타입 역시 const int&
f(rx); // rx는 좌측값, 따라서 T는 const int&,
// param의 타입 역시 const int&
f(27); // 27은 우측값, 따라서 T는 int,
// 그러므로 param의 타입은 int&&
// 보편 참조(universal reference)가 관여하는 경우에는
// 좌측값 인수와 우측값 인수에 대해
// 서로 다른 추론 규칙들이 적용
// 된다.
// 보편 참조(universal reference)가 아닌 매개변수들에
// 대해서는 그런 일이 절대 발생하지
// 않는다.ParamType이 포인터도 아니고 참조도 아닌 경우
ParamType이 포인터도 아니고 참조도 아니라면, 인수가 함수에 값으로 전달(pass-by-value)되는 상황인 것이다. 따라서 param은 주어진 인수의 복사본, 즉 완전히 새로운 객체이다.
param이 새로운 객체라는 사실 때문에, expr에서 T가 추론되는 과정에서 다음과 같은 규칙들이 적용된다.
1. 이전처럼, 만일 expr의 타입이 참조이면, 참조 부분은 무시된다. 1. expr의 참조성을 무시한 후, 만일 expr이 const이면 그 const 역시 무시한다. 만일 volatile이면 그것도 무시한다.
결론적으로 값 전달 방식의 매개변수에 대한 타입 추론 과정에서 const 또는 volatile(또는 그 둘 다인) 인수는 비 const, 비 volatile 인수로 취급된다.
template <typename T>
void f(T param); // 이번에는 param이 값으로 전달된다.
int x = 27; // 이전과 동일
const int cx = x; // 이전과 동일
const int& rx = x; // 이전과 동일
f(x); // T와 param의 타입은 둘 다 int
f(cx); // 여전히 T와 param의 타입은 둘 다 int
f(rx); // 이번에도 T와 param의 타입은 둘 다 int
// cx와 rx가 const 값을 지정하지만,
// 그래도 param은 const가 아님을
// 주목하자.
// param은 cx나 rx의 복사본이므로,
// 다시 말해 param은 cx나 rx와는
// 완전히 독립적인 객체이므로,
// 이는 당연한 결과이다.
// 여기서 명심할 것은, const가(그리고
// volatile이) 값 전달 매개변수에
// 대해서만 무시된다는 점이다.
// expr이 const 객체를 가리키는
// const 포인터이고 param에 값으로
// 전달되는 경우는 어떨까?
// 이런 예를 보자.
template <typename T>
void (T param); // 인수는 param에 여전히 값으로 전달된다.
const char* const ptr = // ptr는 const 객체를 가리키는 const 포인터
"Fun with pointers";
f(ptr); // const char * const 타입의 인수를 전달
// 이 경우 포인터 자체(ptr)는 값으로
// 전달된다. 따라서 ptr의 상수성은
// 무시된다.
// 하지만, ptr이 가리키는 객체
// (여기서는 문자열)의 상수성은
// 여전히 보존된다.
// 그 결과 T와 param의 타입은
// 둘 다 const char*로
// 추론된다.템플릿 타입 추론 과정에서 배열이나 함수 이름에 해당하는 인수는 포인터로 붕괴(decay)한다. 단, 그런 인수가 참조를 초기화하는 데 쓰이는 경우에는 포인터로 붕괴(decay)하지 않는다.
배열 인수
template<typename T>
void f(T param) {}
int main()
{
const char name[] = "Tony";
const char* pName = name;
// T 는 const char[4] 가 아닌 const char* 로 연역됨
f(name);
}아래와 같이 T 에 참조자를 달아 주면, f(name) 을 사용할 때 T 를 const char[4] 로 연역할 수 있다.
template<typename T>
void f(T& param) {}또한 배열에 대한 참조를 선언하는 능력을 이용하면 배열에 담긴 원소들의 개수를 추론하는 템플릿을 만들 수 있다.
// 배열의 크기를 컴파일 시점 상수로서 돌려주는 템플릿 함수
// (배열 매개변수에 이름을 붙이지 않은 것은, 이 템플릿에
// 필요한 것은 배열에 담긴 원소의 개수뿐이기 때문이다)
template <typename T, std::size_t N> // constexpr과
constexpr std::size_t arraySize(T (&)[N]) noexcept // noexcept에
{ // 관해서는 다음을
return N; // 보세요.
}
// 항목 15에서 설명하겠지만, 이 함수를
// constexpr로 선언하면 함수 호출의 결과를
// 컴파일 도중에 사용할 수 있게 된다.
// 그러면 다음 예처럼 중괄호 초기화
// 구문으로 정의된, 기존 배열과 같은
// 크기(원소 개수)의 새 배열을 선언하는
// 것이 가능해진다.
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals의 원소
// 개수는 7
int mappedVals[arraySize(keyVals)]; // mappedVals의 원소
// 개수 역시 7
// 물론 현대적인 C++ 개발자라면 당연히
// 내장 배열보다 std::array를 선호할 것이다.
std::array<int, arraySize(keyVals)> mappedVals; // mappedVals의
// 크기는 7
// arraySize를 noexcept로 선언한 것은
// 컴파일러가 더 나은 코드를 산출하는 데
// 도움을 주려는 것인데, 자세한 사항은
// 항목 14를 보기 바란다.힘수 인수
C++에서 포인터로 붕괴(decay)하는 것이 배열만은 아니다. 함수 타입도 함수 포인터로 붕괴(decay)할 수 있으며, 지금까지 배열에 대한 타입 추론과 관련해서 논의한 모든 것은 함수에 대한 타입 추론에, 그리고 함수 포인터로의 붕괴(decay)에 적용된다.
void someFunc(int, double); // someFunc는 하나의 함수;
// 타입은 void(int, double)
template <typename T>
void f1(T param); // f1의 param은 값 전달 방식
template <typename T>
void f2(T& param); // f2의 param은 참조 전달 방식
f1(someFunc); // param은 함수 포인터로 추론됨;
// 타입은 void (*)(int, double)
f2(someFunc); // param은 함수 참조로 추론됨;
// 타입은 void (&)(int, double)
// 실제 응용에서 이 점 때문에 뭔가 달라지는
// 경우는 드물지만, 배열에서 포인터로의
// 붕괴(decay)를 알고 있다면 함수에서 포인터로의
// 붕괴(decay)도 알아 두는 것이 좋을 것이다.