* 템플릿 형식의 연역 규칙 숙지하기
어떤 복잡한 시스템의 사용자가 그 시스템의 작동 방식은 알지 못해도 그 시스템이 하는 일에 만족한다면, 그 시스템은 설계가 잘 된 것이라 할 수 있다. 그런 관점에서 C++의 템플릿 형식 연역은 성공작이다. 수백 만의 프로그래머들이 템플릿 함수에 인수들을 전달한다.
장점 : 현대적 C++의 아주 강력한 기능 중 하나인 auto가 템플릿에 대한 형식 영역을 기반으로 작동한다.
단점 : 템플릿 형식 연역 규칙들이 auto의 문맥에 적용될 때에는 템플릿에 적용될 때에 비해 덜 직관적인 경우가 있다.
그래서 auto를 잘 활용하려면 auto가 기초하고 있는 템플릿 형식 연역의 면모를 제대로 이해하는 것이 핵심이다.
1) ParamType이 포인터 또는 참조 형식이지만 보편 참조는 아닌 경우
2) ParamType이 보편 참조인 경우
3) ParamType이 포인터도 아니고 참조도 아닌 경우
위 세 가지 형식 연역 시나리오를 살펴보고 템플릿을 사용해야 한다.
<의사 코드>
template<typename T>
void f(PramType param);
그리고 이를 호출하는 코드는 대체로 아래와 같은 모습이다.
f(expr); // 어떤 표현식으로 f를 호출
* 배열 인수
템플릿 형식 연역이 관여하는 대부분의 상황은 지금까지 이야기한 경우에 해당한다. 그러나 그 외에도 알아 둘 필요가 있는 상황이 존재하는데, 배열과 포인터를 구분하지 않고 사용할 수 있는 경우가 있긴 하지만, 배열 형식은 포인터 형식과 다르다는 사실에서 비롯된 것이다. 배열과 포인터를 맞바꿔 쓸 수 있는 것처럼 보이는 환상의 주된 원인은, 많은 문맥에서 배열이 배열의 첫 원소를 가리키는 포인터로 붕괴한다는 점이다.
<의사 코드>
const char name[] = "j.p.briggs"; // name 형식은 const char[13]
const char * ptrToName = name; // 배열이 포인터로 붕괴된다
흥미롭게도, 배열에 대한 참조를 선언하는 능력을 이용해 배열에 담긴 원소들의 개수를 연역하는 템플릿을 만들 수 있다.
// 배열의 크기를 컴파일 시점 상수로서 돌려주는 템플릿 함수
// (배열 매개변수에 이름을 붙이지 않은 것은, 이 템플릿에 필요한 것은 배열에 담긴 원소의 개수뿐이기 때문이다)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
rerutn N;
}
constexpr과 noexcept에 관한 개념은 이해하고 있다고 생각한다.
혹시 설명이 필요하다면 본문에 더 자세하게 나와있다.
위 함수를 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
* 함수 인수
C++에서 포인터로 붕괴하는 것이 배열만은 아니다. 함수 형식도 함수 포인터로 붕괴할 수 있으며, 지금까지 배열에 대한 형식 연역과 관련해서 논의한 모든 것은 함수에 대한 형식 영역에, 그고 함수 포인터로의 붕괴에 적용된다. 배열에서 포인트로의 붕괴를 알고 있다면 함수에서 포인터로의 붕괴도 알아 두는 것이 좋을 것이다.
<의사 코드>
void someFunc(int, double); // someFunc는 하나의 함수, 형식은 void(int, double)
template<typename T>
void f1(T param); // f1의 param은 값 전달 방식
template<thpename T>
void f2(T&* param); // f2의 param은 참조 전달 방식
f1(someFunc); // param은 함수 포인터로 연역됨, 형식은 void (*)(int, double)
f2(someFunc); // param은 함수 참조로 연역됨, 형식은 void (&)(int, double)
템플릿 형식 연역에 대한 auto 관련 규칙들을 모두 살펴보았다. 도입부에서 이 규칙들이 상당히 간단하다고 언급했는데, 실제로 대부분의 규칙은 상당히 간단했다. 단, 보편 참조에 대한 형식을 연역할 때 왼값과 관련된 특별한 규정 때문에 물이 조금 흐려졌으며, 배열과 함수의 포인터 붕괴 규칙 역시 혼란을 좀 더 가중할 수 있었다. 종종 컴파일러에게 (어떤 형식을 연역했는지) 알려달라고 외치고 싶을 때도 있을텐데 (4)항목 에서는 바로 그런 정보를 토해내도록 컴파일러를 설득하는 방법을 집중해서 다룰 것이다.
<포인터>
종종 "X를 가리키는 포인터"를 줄여서 "X에 대한 포인터"로, 또는 더 줄여서 "X 포인터"로 표기할 수 있으니 참고하면 좋겠다. 비슷하게, 종종 "X를 지칭하는 참조"를 "X에 대한 참조" 또는 더 줄여서 "X 참조"로 표기한다. 단, CONST 포인터는 항상 그 자체가 상수인 포인터(CONST를 가리키는 포인터가 아니라)를 뜻하며, volatile 포인터 역시 항상 그 자체가 휘발성인 포인터이다.
*** KeyPoint ***
- 템플릿 형식 연역 도중에 참조 형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
- 보편 참조 매개변수에 대한 형식 연역 과정에서 왼 값 인수들은 특별하게 취급된다.
- 값 전달 방식의 매개변수에 대한 형식 연역 과정에서 const 또는 volatile(또는 그 둘 다인) 인수는 비 const, 비 volatile 인수로 취급된다.
- 템플릿 형식 연역 과정에서 배열이나 함수 이름에 해당하는 인수는 포인터로 붕괴한다. 단, 그런 인수가 참조를 초기화하는데 쓰이는 경우에는 포인터로 붕괴하지 않는다.
'컴퓨터과학' 카테고리의 다른 글
[Effective C++] (5-1) 명시적 형식 선언보다 auto 선호하기 (0) | 2020.05.23 |
---|---|
[Effective C++] (4-2) 연역된 형식을 파악하는 방법 (0) | 2020.05.23 |
[Effective C++] (4-1) 연역된 형식을 파악하는 방법 (0) | 2020.05.23 |
[Effective C++] (3) decltype의 작동 방식 숙지하기 (0) | 2020.05.23 |
[Effective C++] (2) auto의 형식 연역 규칙 숙지하기 (0) | 2020.05.22 |