📌 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)
👉 배열에서 포인터로의 붕괴를 알고 있다면 함수에서 포인터로의 붕괴도 알아 두는 것이 좋음