[Effective Modern C++] 1. 템플릿 타입 추론 규칙을 숙지하자
아래는 함수 템플릿의 선언과 호출부의 의사 코드이다.
template<typename T>
void f(ParamType param);
f(expr); // f를 호출
컴파일 도중 컴파일러는 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의 형태에도 의존하는데, 그 형태에 따라 총 세 가지 경우로 나뉜다.
Case 1: 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*
Case 2: ParamType이 보편 참조인 경우
1. 만일 expr이 lvalue이면, T와 ParamType 둘 다 lvalue 참조로 추론된다. 이는 두 가지 이유로 색다른 상황이다. 첫 번째로 템플릿 타입 추론에서 T가 참조 타입으로 추론되는 경우는 이것이 유일하다. 둘째로 ParamType의 선언 구문은 rvalue 참조와 같은 모습이지만, 추론된 타입은 lvalue 참조이다.
2. 만일 expr이 rvalue이면 일반적인(Case 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는 lvalue, 따라서 T는 int&, param의 타입 역시 int&
f(cx); // cx는 lvalue, 따라서 T는 const int&, param의 타입 역시 const int&
f(rx); // rx는 lvalue, 따라서 T는 const int&, param의 타입 역시 const int&
f(27); // 27은 rvalue, 따라서 T는 int, 그러므로 param의 타입은 int&&
보편 참조(universal reference)가 관여하는 경우에는 lvalue와 rvalue에 대해 서로 다른 추론 규칙들이 적용된다.
그리고 보편 참조(universal reference)가 아닌 매개변수들에 대해서는 그런 일이 절대 발생하지 않는다.
Case 3: ParamType이 포인터도 아니고 참조도 아닌 경우
ParamType이 포인터도 아니고 참조도 아니라면, 인수가 함수에 값으로 전달되는 상황(pass-by-value)인 것이다. 따라서 param은 주어진 인수의 복사본, 즉 완전히 새로운 객체이다.
param이 새로운 객체라는 사실 때문에, expr에서 T가 추론되는 과정은 다음과 같은 규칙들이 적용된다.
1. 이전처럼, 만일 expr의 타입이 참조이면, 참조 부분은 무시된다.
2. expr의 참조성을 무시한 후, 만일 expr이 const이면 그 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 = "Fun with pointers"; // ptr는 const 객체를 가리키는 const 포인터
f(ptr); // const char * const 타입의 인수를 전달
// 이 경우 포인터 자체(ptr)는 값으로 전달된다. 따라서 ptr의 상수성은 무시된다.
// 하지만, ptr이 가리키는 객체 (여기서는 문자열)의 상수성은 여전히 보존된다.
// 그 결과 T와 param의 타입은 둘 다 const char*로 추론된다.
배열 인수
배열 타입의 함수 매개변수라는 것은 없다. 물론 다음과 같은 구문 자체는 적법하다.
void myFunc(int param[]);
그러나 이 경우 배열 선언은 하나의 포인터 선언으로 취급된다. 즉, myFunc의 선언은 사실상 다음과 같은 의미이다.
void myFunc(int* param);
이처럼 배열 매개변수 선언이 포인터 매개변수처럼 취급되므로, 템플릿 함수에 값으로 전달되는 배열의 타입은 포인터 타입으로 추론된다.
즉, 아래의 템플릿 f의 호출에서 타입 매개변수 T는 const char*로 추론된다.
template<typename T>
void f(T param);
const char name[] = "J. P. Briggs";
f(name); // name은 배열이지만 T와 param은 const char*로 추론된다.
여기서 한 가지 요령이 있는데, 함수의 매개변수를 진짜 배열로 선언할 수는 없지만, 배열에 대한 참조로 선언할 수는 있다.
template<typename T>
void f(T& param);
f(name); // 배열을 f에 전달
이렇게 하면 T에 대해 추론된 타입은 배열의 실제 타입이 된다. 그 타입은 배열의 크기를 포함하므로, 위의 예시에서 T는 const char [13]으로 추론되고 f의 매개변수(그 배열에 대한 참조)의 타입은 const char(&)[13]으로 추론된다.
이렇게 배열에 대한 참조를 선언하는 능력을 이용하면 배열에 담긴 원소들의 개수를 추론하는 템플릿을 만들 수 있다.
// 배열의 크기를 컴파일 시점 상수로서 돌려주는 템플릿 함수 (배열 매개변수에 이름을 붙이지 않은 것은, 이 템플릿에 필요한 것은 배열에 담긴 원소의 개수뿐이기 때문이다)
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
// constexpr와 noexcept
// 항목 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)도 알아 두는 것이 좋을 것이다.
붕괴(decay)
수식에서 사용될 때 포인터로 변환되는 현상.
붕괴란 암시적 변환에 의해서 발생하는 현상으로 크기 정보를 잃어버리고 첫 번째 주소를 가리키는 포인터로 변환된다는 것을 뜻한다.
요약
- 템플릿 타입 추론 도중에 참조 타입의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
- 보편 참조 매개변수에 대한 타입 추론 과정에서 lvalue 인수들은 특별하게 취급된다.
- 값 전달 방식의 매개변수에 대한 타입 추론 과정에서 const 또는 volatile(또는 그 둘 다)인 인수는 비 const, 비 volatile 인수로 취급된다.
- 템플릿 타입 추론 과정에서 배열이나 함수 이름에 해당하는 인수는 포인터로 붕괴(decay)한다. 단, 그런 인수가 참조를 초기화하는 데 쓰이는 경우에는 포인터로 붕괴(decay) 하지 않는다.
'Books > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 6. auto가 원치 않은 타입으로 추론 될 때에는 명시적 타입의 초기화를 생각하자 (0) | 2022.08.27 |
---|---|
[Effective Modern C++] 5. 명시적 타입 선언보다는 auto를 선호하자 (0) | 2022.08.15 |
[Effective Modern C++] 4. 추론된 타입을 파악하는 방법을 알아두자 (0) | 2022.08.06 |
[Effective Modern C++] 3. decltype의 작동 방식을 숙지하자 (0) | 2022.08.06 |
[Effective Modern C++] 2. auto의 타입 추론 규칙을 숙지하자 (0) | 2022.08.06 |
댓글
이 글 공유하기
다른 글
-
[Effective Modern C++] 5. 명시적 타입 선언보다는 auto를 선호하자
[Effective Modern C++] 5. 명시적 타입 선언보다는 auto를 선호하자
2022.08.15 -
[Effective Modern C++] 4. 추론된 타입을 파악하는 방법을 알아두자
[Effective Modern C++] 4. 추론된 타입을 파악하는 방법을 알아두자
2022.08.06 -
[Effective Modern C++] 3. decltype의 작동 방식을 숙지하자
[Effective Modern C++] 3. decltype의 작동 방식을 숙지하자
2022.08.06 -
[Effective Modern C++] 2. auto의 타입 추론 규칙을 숙지하자
[Effective Modern C++] 2. auto의 타입 추론 규칙을 숙지하자
2022.08.06