
📌 Main Point
- 템플릿 형식 연역 도중에 참조 형식의 인수들은 비참조로 취급됨 (= 참조성이 무시됨)
- 보편 참조 매개변수에 대한 혁식 연역 과정에서 왼값 인수들은 특별하게 취급됨
- 값 전달 방식의 매개변수에 대한 형식 연역 과정에서
const또는volatile인수는 비const, 비volatile인수로 취급됨- 템플릿 형식 연역 과정에서 배열이나 함수 이름에 해당하는 인수는 포인터로 붕괴함 (단, 그런 인수가 참조를 초기화하는데 쓰이는 경우에는 포인터로 붕괴하지 않음)
auto는 template에 대한 형식 연역을 기반으로 작동함template, C++11에서는 auto에 대한 형식 연역이 가능template 형식 연역 규칙들이 auto의 문맥에 적용될 때에는 template에 비해 덜 직관적인 경우가 있음auto를 잘 활용하려면 auto가 기초하고 있는 템플릿 형식 연역을 제대로 이해해야 함✍ Ex) 일반적인 함수 템플릿의 선언 및 호출 방식
template<typename T>
void f(ParamType param); // 함수 템플릿 선언
f(expr); // 어떤 표현식(expr)으로 f를 호출
👉 위 코드에 대해 컴파일러는 expr을 이용해서 두 가지 형식(T에 대한 형식 + ParamType에 대한 형식)을 연역함
👉 ParamType에 흔히 const나 참조 한정사 같은 수식어들이 붙기 때문에 두 형식이 다른 경우가 많음
✍ Ex) 템플릿 선언 및 호출 예시
template<typename T>
void f(const T& param); // ParamType은 const T&
int x = 0;
f(x); // int로 f를 호출, expr로부터 T와 ParamType을 연역
👉 이 경우 T는 int로 연역되지만 ParamType은 const int&로 연역됨
👉 T에 대해 연역된 형식이 함수에 전달된 인수의 형식과 같을 것 (= T가 expr의 형식)이라고 예상하는 것은 당연함 (위 코드에서 x는 int이고 T는 int로 연역됨)
⛔ 하지만 항상 그런 것은 아님
🔑 T에 대해 연역된 형식은 expr의 형식에 의존할 뿐만 아니라 ParamType의 형태에도 의존함
ParamType이 포인터 형식이나 참조 형식이지만 보편 참조는 아닌 경우임expr이 참조 형식이면 참조 부분을 무시expr의 형식을 ParamType에 대해 패턴 부합 (pattern-matching) 방식으로 대응시켜서 T의 형식을 결정✍ Ex) Case 1-1. param이 참조 형식인 경우
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에 대한 참조
f(x); // T는 int, param의 형식은 int&
f(cx); // T는 const int, param의 형식은 const int&
f(rx); // T는 const int, param의 형식은 const int&
👉 2-3번째 호출에서 cx와 rx에 const 값이 배정되었기 때문에 T가 const int로 연역되었고, 매개변수 param의 형식은 const int&가 되었음
🔑 이것은 호출자에게 중요한 문제인데, const 객체를 참조 매개변수에 전달하는 호출자는 그 객체가 수정되지 않을 것이라고 기대함 (= 매개변수가 const에 대한 참조일 것이라고 기대함)
🔑 T& 매개변수를 받는 템플릿에 const 객체를 전달해도 안전한 이유가 바로 이것임
🔑 객체의 const성(constness; 상수성)은 T에 대해 연역된 형식의 일부가 됨
👉 3번째 호출에서 rx의 형식이 참조이지만 T는 비참조로 연역되었음 (형식 연역 과정에서 rx의 참조성이 무시되기 때문)
✍ Ex) Case 1-2. param이 const에 대한 참조 형식인 경우
template<typename T>
void f(const T& param); // param은 const에 대한 참조
int x = 27; // x는 int
const int cx = x; // cx는 const int
const int& rx = x; // rx는 const int인 x에 대한 참조
f(x); // T는 int, param의 형식은 const int&
f(cx); // T는 int, param의 형식은 const int&
f(rx); // T는 int, param의 형식은 const int&
🔑 f의 매개변수의 형식을 T&에서 const T&로 바꾸면 상황이 조금 달라지긴 하지만,cx와 rx의 const성은 계속 유지됨
🔑 param이 const에 대한 참조로 간주되므로, const가 T의 일부로 연역될 필요는 없음
👉 Case 1-1처럼 형식 연역 과정에서 rx의 참조성은 무시됨
✍ Ex) Case 1-3. param이 포인터 형식(const를 가리키는 포인터)인 경우
template<typename T>
void f(T* param); // param은 포인터
int x = 27; // x는 int
const int *px = &x; // px는 const int로서의 x를 가리키는 포인터
f(&x); // T는 int, param의 형식은 int*
f(px); // T는 const int, param의 형식은 const int*
👉 param이 참조가 아니라 포인터(또는 const를 가리키는 포인터)라도 형식 연역은 본질적으로 같은 방식으로 진행
T를 받는 함수 템플릿에서 보편 참조의 선언 형식은 T&&임), 왼값 인수가 전달되면 오른값 참조와는 다른 방식으로 행동expr이 lvalue(왼값)이면, T와 ParamType 둘 다 왼값 참조로 연역됨 (이중으로 비정상적인 상황)T가 참조 형식으로 연역되는 경우는 이것이 유일함ParamType의 선언 구문은 오른값 참조와 같은 모습이지만, 연역된 형식은 왼값 참조임expr이 rvalue(오른값)이면, 정상적인 (= Case 1의) 규칙들이 적용됨✍ Ex) Case 2. param이 보편 참조인 경우
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에 대한 참조
f(x); // x는 왼값(lvalue),
// 따라서 T는 int&, param의 형식은 const int&
f(cx); // cx는 왼값(lvalue),
// 따라서 T는 const int&, param의 형식은 const int&
f(rx); // 27는 오른값(rvalue),
// 따라서 T는 int, param의 형식은 int&&
👉 이 예제에서는 보편 참조 매개변수에 관한 형식 연역 규칙들이 왼값 참조나 오른값 참조 매개변수들에 대한 규칙들과는 다르다는 점만 알고 넘어 감 (Item 24에서 자세한 이유를 설명)
🔑 보편 참조가 관여하는 경우에는 왼값 인수와 오른값 인수에 대해 서로 다른 연역 규칙들이 적용됨 (보편 참조가 아닌 매개변수들에 대해서는 그런 일이 절대 발생하지 않음)
ParamType이 포인터도 아니고 참조도 아니라면, 인수가 함수에 값으로 전달 (pass-by-value)인 상황임param은 주어진 인수의 복사본 (= 완전히 새로운 객체)임param이 새로운 객체이기 때문에, expr에 T가 연역되는 과정에서 다음과 같은 규칙들이 적용expr의 형식이 참조이면, 참조 부분은 무시됨expr의 참조성을 무시한 후, 만약 expr이 const이면 그 const 역시 무시함 (만약 volatile이면 그것도 무시함)✍ Ex) Case 3-1. param이 포인터도 참조도 아닌 경우
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에 대한 참조
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와는 '완전히 독립적' 인 객체이므로) 당연한 결과임
👉 cx와 rx가 수정될 수 없다는 점은 param의 수정 가능 여부와는 전혀 무관함
👉 param의 형식을 연역하는 과정에서 expr의 const성이(volatile 포함) 무시되는 이유임
👉 expr을 수정할 수 없다고 해서, 그 복사본까지 수정할 수 없는 것은 아님
✍ Ex) Case 3-2. expr이 const 객체를 가리키는 const 포인터이고 param에 값으로 전달되는 경우
template<typename T>
void f(T param); // 인수는 param에 값으로 전달
const char* const ptr = // ptr는 const 객체를 가리키는 const 포인터
"Fun with pointers";
f(ptr); // const char * const 형식의 인수를 전달
🔑 ptr 선언의 별표(*) 오른쪽에 있는 const 때문에 ptr 자체는 const 가 됨
👉 즉, ptr를 다른 장소를 가리키도록 변경할 수 없으며, ptr에 널(null)을 배정할 수도 없음 (별표 왼쪽의 const는 ptr이 가리키는 것, 즉 문자열이 const임을 뜻함 → 그 문자열은 변경할 수 없음)
👉 ptr를 f에 전달하면 그 포인터를 구성하는 비트들이 param에 복사됨
= 포인터 자체(ptr)는 값으로 전달됨 (형식 연역 과정에서 ptr의 const성은 무시됨, 값 전달 방식의 매개변수에 관한 형식 연역 규칙과 일치함)
👉 결과적으로 param에 연역되는 형식은 const char*임 (= param은 cosnt 문자열을 가리키는 수정 가능한 포인터)
🔑 형식 연역 과정에서 ptr가 가리키는 것의 const성은 보존되지만, ptr 자체의 const성은 ptr를 복사해서 새 포인터 param을 생성하는 도중에 사라짐
배열이 배열의 첫 원소를 가리키는 포인터로 붕괴한다(decay)는 점임✍ Ex) 배열과 배열의 첫 원소를 가리키는 포인터
const char name[] = "J. P. Briggs"; // name의 형식은 const char[13]
const char * ptrToName = name; // 배열이 포인터로 붕괴
👉 const char* 형식의 포인터 ptrToName을 name으로 초기화하는데, name 자체는 const char[13] 형식의 배열임
👉 두 형식(const char*와 const char[13])은 서로 같지 않지만, 배열에서 포인터로의 붕괴 규칙 때문에 이 코드가 형식 불일치 오류 없이 컴파일됨
✍ Ex) 값 전달 매개변수를 받는 템플릿에 배열을 전달하면?
template<typename T>
void f(T param); // 값 전달 매개변수가 있는 템플릿
const char name[] = "J. P. Briggs"; // name의 형식은 const char[13]
f(name); // T와 param에 대해 연역되는 형식들은?
// name은 배열이지만 T는 const char*로 연역됨
🔑 배열 형식의 함수 매개변수라는 것은 없음!
void myFunc(int param[]); // 이 구문 자체는 문제가 없지만,
// 이 경우 배열 선언은 하나의 포인터 선언으로 취급됨
void myFunc(int* param); // 위와 동일한 함수
👉 배열 매개변수 선언이 포인터 매개변수처럼 취급되므로, 템플릿 함수에 값으로 전달되는 배열의 형식은 포인터 형식으로 연역됨 (즉, 템플릿 f의 호출에서 형식 배개변수 T는 const char*로 연역됨)
✍ Ex) 함수의 매개변수를 진짜 배열로 선언할 수는 없지만, 배열에 대한 참조로 선언할 수는 있음
template<typename T>
void f(T& param); // 참조 전달 매개변수가 있는 템플릿
const char name[] = "J. P. Briggs"; // name의 형식은 const char[13]
f(name); // 배열 name을 f에 전달
🔑 위의 코드처럼 템플릿 f가 인수를 참조로 받도록 수정하고, 함수에 배열을 전달하면 T에 대해 연역된 형식은 배열의 실제 형식이 됨
👉 그 형식은 배열의 크기를 포함하므로, 위 코드에서 T는 const char [13]으로 연역되고, f의 매개변수의 형식은 const char (&)[13]으로 연역됨
✍ Ex) 배열에 대한 참조를 선언하는 능력을 이용하면 배열에 담긴 원소들의 개수를 연역하는 템플릿을 만들 수 있음
// 배열의 크기를 컴파일 시점 상수로서 돌려주는 템플릿 함수
// (배열 매개변수에 이름을 붙이지 않은 것은, 이 템플릿에
// 필요한 것은 배열에 담긴 원소의 개수뿐이기 때문)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
🔑 Item 15에서 설명하겠지만, 이 함수를 constexpr로 선언하면 함수 호출의 결과를 컴파일 도중에 사용할 수 있게 됨 (arraySize를 noexcept로 선언한 것은 컴파일러가 더 나은 코드를 산출하는 데 도움을 주려는 것임, Item 14에서 자세한 내용 알 수 있음)
👉 그러면 아래 코드처럼 중괄호 초기화 구문으로 정의된, 기존 배열과 같은 크기(원소 개수)의 새 배열을 선언하는 것이 가능해짐
int keyVals[] = {1, 3, 7, 9, 11, 22, 35}; // keyVals의 원소 개수는 7
int mappedVals[arraySize(keyVals)]; // mappedVals의 원소 개수 역시 7
std::array<int, arraySize(keyVals)> mappedVals // 내장 배열 대신 std::array를 사용한 경우
✍ Ex) 함수에 대한 형식 연역에 배열에 대한 형식 연역을 적용
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)
👉 배열에서 포인터로의 붕괴를 알고 있다면 함수에서 포인터로의 붕괴도 알아 두는 것이 좋음