※ 형식연역 (type deduction)

형식연역이 일어나는 방식을 확실히 이해하지 않는다면 Modern C++에서 효과적인 프로그래밍은 불가능하다.

1장에서는 이런 형식연역의 작동방식과 형식연역에 기초한 auto와 decltype의 작동방식에 대해 알아본다.

필요성 : 템플릿 형식 연역 규칙들이 auto의 문액에 적용 될 때 덜 직관적이며
auto를 잘 활용하려면 템플릿 형식 연역을 이해하고 있어야 한다.

 

 

 

 

 

User가 System의 작동방식을 알지 못해도 System의 일에 만족한다면, 잘 설계되었다라고도 할 수 있다.

 

그런 프로그래머라면 좋은 소식과 나쁜 소식이 있다.

- Positive: Modern C++은 아주 강한 기능 중 하나인 auto가 template 형식영역을 기반으로 작동한다는 것이다.

- Negative: template형식 연역규칙들이 auto의 문맥에 적용될 때 template에 비해 덜 직관적인 경우가 있다.

 

∴ 따라서 auto를 잘 활용하려면 auto가 기초하는 template 형식연역의 면모를 제대로 이해해야한다.

 

 

 

※ 함수 template

template<typename T>
void f(ParamType param);

// 어떤 표현식으로 f를 호출
f(expr);

컴파일 도중 expr을 이용해 2가지의 형식을 연역한다.

1. T에 대한 형식

2. ParamType에 대한 형식

 

이 두 형식이 다른 경우가 많은데 이는 ParamType에 흔히 const 같은 참조한정사 수식어가 붙기 때문이다.

template<typename T>
void f(const T& param); //ParamType은 const T&

int x = 0;

f(x); // int로 f를 호출

이 경우, T는 int로 연역되지만 ParamType은 cosnt int& 로 연역된다. (x는 int이고 T는 int로 연역된다.)

 

T에 대해 연역된 형식이 함수에 전달된 인수의 형식과 같다고 생각했다면 그것이 항상 그러지는 않는다.

=> T에 대해 연역된 형식은 expr의 형식에 의존할 뿐만 아니라 ParamType의 형태에도 의존한다.

그 형태에 따라 총 3가지 경우로 나뉜다.

1. ParamType이 포인터, 참조형식이지만 보편참조는 아닌 경우.
2. ParamType이 보편참조인 경우
3. ParamType이 포인터, 참조 모두 아닌 경우

따라서 3가지 형식 연역 시나리오에 대해 살펴봐야 한다.

 

다음과 같은 일반적인 형태의 템플릿과 그 호출에 기초해 3가지 시나리오를 생각해보자.

template<typename T>
void f(ParamType param);

f(expr);  // expr로부터 T와 ParamType을 연역

 

 

Case 1. ParamType이 포인터, 참조형식 이지만 보편참조는 아님.

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는 cosnt int, param의 형식은 const int&

위 f(cx), f(rx)의 param은 const값이고, 이는 그 객체가 수정되지 않을 것이라 기대한다.

즉, 해당 매개변수가 const에 대한 참조일 것이라 기대한다. (T& 매개변수를 받는 템플릿에 const객체를 전달해도 안전한 이유)

 

f(rx)에서 비록 rx의 형식이 참조이지만 T는 비참조로 연역된 것에 주목하면 이는 형식연역과정에서 rx의 참조성이 무시되기 때문임을 알 수 있다.

만약 f의 매개변수 형식을 T&에서 const T&로 바꾸게 된다면 상황이 달라지지만 큰 변화는 없다.
cx와 rx의 const성은 유지되며 단, param이 const에 대한 참조로 간주되므로  
const가 T의 일부로 연역될 필요는 없다.
template<typename T>
void f(const T& param);  // param이 cosnt에 대한 참조

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의 형식은 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이 cosnt에 대한 참조

int x = 27;        // x는 int
const int *px = x; // px는 const int로 x를 가리키는 포인터

/* 여러 호출에서 param과 T에 대해 연역된 형식 */
f(&x);  // T는 int, param의 형식은 int*
f(px); // T는 const int, param의 형식은 const int*

 

여기까지는 너무 쉬워서 하품이 날 지경이라고 한다.(물론 저자가 그런거지 난 아니다. 어려워 죽을거 같다.)

 

 

 

 

 

 

Case 2. ParamType이 보편참조임.

템플릿이 보편참조매개변수를 받는 경우, 상황이 불투명해진다.

그런 매개변수 선언은 rvalue 참조와 같은 모습이다. (보편참조의 선언형식은 T&&이다.)

l-value가 전달되면 r-value참조와는 다른 방식으로 행동한다.

 

- 만약 expr이 l-value면, T와 ParamType 둘 다 l-value 참조로 연역된다. (비정상적 상황)

  i) 템플릿 형식 연역게서 T가 참조형식으로 연역되는 경우, 이것이 유일

  ii) ParamType의 선언구문은 r-value 참조와 같지만 연역된 형식은 l-value참조이다.

- 만약 expr이 r-value면, '정상적인' 규칙들이 적용된다. (Case 1의 규칙들.)

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는 l-value, 따라서 T는 int&, param 형식도 int&
f(cx); // cx는 l-value, 따라서 T는 const int&, param 형식도 const int&
f(rx); // rx는 l-value, T는 const int&, param 형식도 const int&
f(27); // 27은 r-value, 따라서 T는 int, param 형식은 int&&

나중에 item 24에서 설명될 것이고, 지금은 보편참조매개변수에 관한 형식연역 규칙들이

l-value참조나 r-value참조 매개변수들에 대한 규칙들과는 다르다는 점만 기억하면 된다.

특히 보편참조가 관여하는 경우, l-value와 r-value에 대해 서로 다른 연역규칙이 적용된다. (보편참조가 아니면 발생X)

 

 

 

 

 

 

 

Case 3. ParamType이 포인터도 아니고 참조도 아님

ParamType이 포인터도 참조도 아닌 경우, 인수가 함수에 값으로 전달되는 pass-by-value인 상황이다.

template<typename T>
void f(T param);     // param이 값으로 전달된다.

따라서 param 주어진 인수의 복사본 (즉, 완전히 새로운 객체)이다.

param이 새로운 객체라는 사실 때문에 expr에서 T가 연역되는 과정에서 다음과 같은 규칙들이 적용된다.

1. 이전처럼, 만약 expr의 형식이 참조라면, 참조부분은 무시된다.
2. expr의 참조성을 무시한 후 만약 expr이 const라 해도 그 const 역시 무시한다.

만약 volatile객체라도 그것도 무시한다.   (volatile객체는 흔하지 않기에 (장치구동기 구현시 사용) item 40 참고)

 

다음은 이 규칙들이 적용되는 예이다.

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가 수정될 수 없다는 점(const)은 param의 수정가능 여부와는 무관하기 때문이다.

param의 형식을 연역하는 과정에서 expr의 const성이 무시되는 이유가 바로 이것이다.

expr을 수정할 수 없다해서 그 복사본까지 수정할 수 없는 것은 아니기 때문이다.

 

여기서 명심할 점은, const가 값 전달 매개변수에 대해서만 무시된다는 점이다.

즉, const에 대한 참조나 포인터 매개변수의 경우, 형식연역과정에서 expr의 const성이 보존된다.

 

 

그러나 expr이 cosnt객체를 가리키는 const포인터이고 param에 값으로 전달되는 경우는 어떨까?

template<typename T>
void f(T param);                    // 인수는 param에 여전히 값으로 전달됨

const char* const ptr = "V2LLAIN";  // ptr은 const 객체를 가리키는 const 포인터

f(ptr);                             // const char* const 형식의 인수를 전달

포인터 선언 오른쪽 const로 ptr 자체가 const가 된다. (const char* const ptr)

즉, ptr을 다른 장소를 가리키도록 변경불가능! (nullptr도 배정할 수 없다.)

 

포인터 선언 왼쪽 const가 가리키는 것은 문자열이 const인 것을 의미한다. (const char* const ptr)

 

ptr을 f에 전달하면 그 포인터를 구성하는 bit들이 param에 복사되며 포인터자체(ptr)는 값으로 전달된다.

형식연역과정에서 ptr의 const성은 무시되며 따라서 param에 연역되는 형식은 const char*이다.

즉, 형식연역과정에서 ptr이 가리키는 것의 const성은 보존되나

ptr 자체의 const성은 ptr을 복사해 새 포인터 param을 생성하는 도중 사라진다!

 

 

 

※ 템플릿 연역과정에서 배열이나 함수이름에 해당하는 인수는 포인터로 붕괴(decay)한다.

(단, 그런 인수가 참조를 초기화하는데 사용되면, 그 경우에는 포인터로 붕괴하지 않는다.)

 

§ 배열과 포인터를 구분하지 않고 사용가능하지만, 배열형식은 포인터 형식과 다르다는 사실!

- 배열과 포인터를 맞바꿔 쓸 수 있는 것처럼 보이는 환상의 주된 원인은 많은 문맥에서 배열이 배열의 첫 원소를 가리키는 포인터로 붕괴한다(decay)는 점이다.

이런 붕괴때문에 아래와 같은 코드는 오류가 없이 컴파일 된다.

const char name[] = "V2LLAIN";
const char* ptrname = name;

 

그런데 배열을 값 전달 매개변수로 받는 template에 전달하면 어떻게 될까?

우선 배열형식의 함수 매개변수라는 것은 없다!

물론 아래와 같은 구문 자체는 가능하다.

void myFunc(int param[]);

하지만 이 경우 배열선언은 하나의 포인터 선언으로 취급되므로 사실상 아래와 동일하게 취급된다.

void myFunc(int *param);  // 위와 동일한 함수

 

따라서 아래 예시를 보면 name은 배열이지만 T는 const char*로 연역되어 버린다.

const char name[] = "V2LLAIN";

template<typename T>
void f(T param);      
f(name);             // name은 배열이지만 T는 const char*로 연역됨

 

 

그런데 한가지 교묘한 요령이 있는데, 함수의 매개변수를 진짜 배열로 선언은 못하지만,

배열에 대한 참조로 선언할 수는 있다!

 

즉, 다음과 같이 템플릿 f가 인수를 참조로 받도록 수정하고 함수에 배열을 전달하면

T에 대해 연역된 형식은 배열의 실제형식이 된다!

const char name[] = "V2LLAIN";

template<typename T>
void f(T& param);      // 참조 전달 매개변수가 있는 템플릿
f(name);               // 배열을 f에 전달

 

배열에 대한 참조선언을 이용하면 배열에 담긴 원소들의 개수를 연역하는 템플릿을 만들 수 있다!

 

[item 15]에서 더 자세히 나오지만 이 함수를 constexpr로 선언하면 함수 호출의 결과를 컴파일 도중 사용할 수 있다!

그렇게 되면 기존 배열과 같은 크기의 새 배열을 선언하는 것이 가능하게 된다.

template<typename T, std::size_t N>

constexpr std::size_t arraySize(T (&) [N]) noexcept { return N; }

int Vals[] = {1, 2, 3, 4 ,5, 6, 7};  // Vals의 원소개수는 7
int mapVals[arraySize(Vals)];  // mapVals의 원소 개수 역시 7개

/*물론, modern C++ 개발자라면 std::array나 vector를 더 선호할 것이다.*/
std::array<int, arraySize(Vals)> mapVals;  // mapVals의 크기는 7

이때, arraySizenoexcept 선언한 것은 컴파일러가 더 나은 코드를 산출하는데 도움을 주기 위한 것이다. (item 14)

 

 

물론, C++에서 포인터로 붕괴하는 것이 배열만 있는 것은 아니다.

앞에서 배열에 대한 형식연역과 관련된 모든것은 함수포인터로의 붕괴에 적용된다.

void someFunc(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)

 

 

 

 

 

 

◈ Remind ◈

- 템플릿형식 연역 중 참조형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
- 보편참조 매개변수에 대한 형식 연역과정에서 l-value들은 특별하게 취급된다.
- 값 전달방식의 매개변수에 대한 형식연역과정에서 const, volatile 인수는 비 const, 비 volatile 인수로 취급된다.
- 템플릿형식 연역과정에서 배열이나 함수이름에 해당하는 인수는 포인터로 붕괴한다.
  단, 그런 인수가 참조를 초기화하는데 쓰이는 경우, 포인터로 붕괴하지 않는다.

+ Recent posts