티스토리 뷰


Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14
Meyers, Scott



Item 1 : Understand template type deduction


템플릿 타입 추론은 모던 C++의 auto를 기반으로 한다. 따라서 auto를 이용하면 별다른 고민 없이 사용 가능하다. 문제는 템플릿 타입 추론 규칙을 auto에 적용할 때 직관적으로 이해하기 어려운 동작들이 존재한다는 것이다. 따라서 auto에 기반한 템플릿 타입 추론을 정확히 이해해야 한다. 


template<typename T>

void f(ParamType param);

f(expr);


컴파일하는 동안 컴파일러는 expr을 사용해 T와 ParamType, 두 타입을 추론한다. 하지만 ParamType은 const나 레퍼런스 타입과 같은 키워드를 포함하고 있기 때문에 T와 ParamType은 다르다.


template<typename T>

void f(Const T& param);        // ParamType은 const T&


int x = 0;

f(x);


예제에서는 T는 int로, ParamType은 const int&로 추론된다.

일반적으로, T에 대한 추론 타입은 함수에 전달되는 인수의 타입과 같다고 생각할 수 있다. 하지만, T에 대한 추론 타입은 expr 뿐만 아니라, ParamType에도 영향을 받는다.


ParamType에 따른 세가지 추론 시나리오

  • Case 1 : ParamType이 레퍼런스 또는 포인터 타입이지만, 유니버셜 레퍼런스 타입은 아닌 경우
  • Case 2 : ParamType이 유니버셜 레퍼런스인 경우
  • Case 3 : ParamType이 레퍼런스 또는 포인터가 아닌 경우


Case 1 : ParamType이 레퍼런스 또는 포인터 타입이지만, 유니버셜 레퍼런스 타입은 아닌 경우


1. expr의 타입이 레퍼런스라면, 레퍼런스 부분을 무시함.

2. T의 타입을 결정하기 위해 expr과 ParamType을 비교해 패턴 매칭.


template<typename T> 

void f(T& param);     // param is a reference


int x = 27;                // x is an int 

const int cx = x;       // cx is a const int 

const int& rx = x;      // rx is a reference to x as a const int


f(x);                     // T is int, param's type is int&

f(cx);                   // T is const int, param's type is const int&

f(rx);                    // T is const int, param's type is const int&


f의 매개변수 T&에서 const T&가 되면, 변화가 일어난다.

- cx와 rx를 호출할 때 T의 타입은 const int가 되어야 하지만

  그럴 경우 param의 타입이 const const int 가 되기 때문에 

  패턴 매칭을 통해 T를 const int에서 int로 변경.


template<typename T> 

void f(const T& param);  // param is now a ref-to-const


int x = 27;                    // as before 

const int cx = x;           // as before 

const int& rx = x;          // as before


f(x);                       // T is int, param's type is const int&

f(cx);                     // T is int, param's type is const int&

f(rx);                      // T is int, param's type is const int&


param이 레퍼런스 타입이 아닌 포인터 타입이더라도, 같은 방식으로 동작한다.


template<typename T> 

void f(T* param);        // param is now a pointer


int x = 27;                   // as before 

const int *px = &x;      // px is a ptr to x as a const int


f(&x);                       // T is int, param's type is int*

f(px);                       // T is const int, param's type is const int


Case 2 : ParamType이 유니버셜 레퍼런스인 경우


유니버셜 레퍼런스는 rvalue 레퍼런스(&&)와 같이 선언한다. 유니버셜 레퍼런스는 lvalue과 rvalue 모두를 받을 수 있다. rvalue 인수들이 전달되면 rvalue 레퍼런스로 추론한다. 하지만, lvalue 인수들을이 전달되면 전혀 다르게 행동한다.

- expr이 lvalue라면, T와 ParamType은 lvalue 레퍼런스로 추론됨.

  1. 템플릿 타입 추론에서 T가 레퍼런스 타입일 때만 일어날 수 있는 상황
  2. ParamType이 rvalue 레퍼런스 문법을 사용해 선언됐더라도, 추론된 타입은 lvalue 레퍼런스가 됨.


template<typename T> 

void f(T&& param);       // param is now a universal reference


int x = 27;                  // as before 

const int cx = x;         // as before 

const int& rx = x;        // as before


f(x);                      // x is lvalue, so T is int&, param's type is also int&

f(cx);                    // cx is lvalue, so T is const int&, param's type is also const int&

f(rx);                    // rx is lvalue, so T is const int&, param's type is also const int&

f(27);                   // 27 is rvalue, so T is int, param's type is therefore int&&


Case 3 : ParamType이 레퍼런스 또는 포인터가 아닌 경우

 

ParamType이 레퍼런스 또는 포인터가 아닌 경우, 매개변수는 값에 의한 호출(pass-by-value)이 된다. param은 전달된 값의 복사본이므로  새로운 오브젝트가 된다. 따라서 T는 expr로부터 다음과 같이 추론된다.

- 이전과 같이 expr의 타입이 레퍼런스라면, 레퍼런스 부분을 무시함.

- 만약 expr의 레퍼런스 부분을 무시한 후, expr이 const라면 const 부분도 무시함.

- 만약 volatile이라면, 이 부분 또한 무시함.


int x = 27;              // as before 

const int cx = x;     // as before 

const int& rx = x;    // as before

 

f(x);                     // T's and param's types are both int

f(cx);                   // T's and param's types are again both int

f(rx);                   // T's and param's types are still both int


param은 cx 및 rx와 다른 오브젝트이므로 param이 무엇이든 cx와 rx는 수정할 수 없다.

const 및 volatile은 템플릿에서 매개변수가 값에 의한 호출로 전달할 때만 무시된다. 매개변수가 const 레퍼런스 및 const 포인터라면, expr의 const는 타입 추론 과정에서 보존된다. 만약 expr이 const 오브젝트를 가리키는 const 포인터이고, expr이 값에 의한 호출로 param에 전달되면 어떨까?


template<typename T> 

void f(T param);                                         // param is still passed by value

const char* const ptr = "Fun with pointers";  // ptr is const pointer to const object

f(ptr);                                                       // pass arg of type const char * const


*의 오른쪽에 const : ptr의 상수화! (ptr이 다른 위치를 가리킬 수 없음)

*의 왼쪽에 const : ptr이 가리키는 값의 상수화! (문자열을 수정할 수 없음)

ptr이 가리키고 있는 것의 const는 타입 추론을 하는 동안 보존된다. 하지만, ptr 자체의 const는 새 포인터 param의 생성을 위해 복사할 때 무시된다.


Array Arguments

 

배열과 포인터는 서로 번갈아가며 사용할 수 있지만, 분명히 다른 타입이다. 배열은 첫 번째 요소를 가리키는 포인터로 붕괴될 수 있다.


const char name[] = "J. P. Briggs";   // name's type is const char[13]

const char * ptrToName = name;       // array decays to pointer


여기서 const char* 타입인 ptrToName은 const char[13] 타입인 name으로 초기화 된다. const char*와 const char[13]은 서로 같은 타입은 아니지만 배열 - 포인터 붕괴 규칙으로 인해 컴파일 된다.

만약 배열이 값의 의한 호출을 통해 템플릿의 매개변수로 전달된다면 무슨 일이 일어날까?

함수의 매개변수가 배열인 경우는 없다. 문법적으로 가능하지만 배열의 선언은 포인터의 선언으로 취급된다. 배열과 포인터의 동등성은 C++의 토대인 C에서 비롯된 결과이다.

 

void myFunc(int param[]);

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

 

배열 매개변수 선언이 포인터 매개변수로 취급되기에 템플릿 함수에 값에 의한 호출로 전달된 배열 타입은 포인터 타입으로 추론된다.


f(name);                        // name은 배열이지만, T는 const char*로 추론됨.


하지만 배열을 참조하도록 매개변수를 선언한다면 어떻게 추론될까?

 

template<typename T> 

void f(T& param);       // template with by-reference parameter

f(name);                   // pass array to f

 

이 때 T는 const char[13]으로 추론되고,  f의 매개변수 타입은 const char(&)[13]이 된다.

배열의 레퍼런스 타입을 선언할 수 있는 기능은 템플릿이 배열에 포함된 요소들의 개수를 추론할 수 있도록 만들어 준다.

 

// return size of an array as a compile-time constant. (The 

// array parameter has no name, because we care only about 

// the number of elements it contains.) 

template<typename T, std::size_t N>                           // see info 

constexpr std::size_t arraySize(T (&)[N]) noexcept      // below on

{                                                                            // constexpr

  return N;                                                               // and 

}                                                                           // noexcept

 

Function Arguments

 

배열 뿐만 아니라 함수 타입들도 함수 포인터로 붕괴된다.

 

void someFunc(int, double);                              // someFunc is a function; type is void(int, double)

 

template<typename T> void f1(T param);             // in f1, param passed by value

template<typename T> void f2(T& param);           // in f2, param passed by ref

 

f1(someFunc);                 // param deduced as ptr-to-func; type is void (*)(int, double)

f2(someFunc);                 // param deduced as ref-to-func; type is void (&)(int, double)


Summary

  • 템플릿 타입 추론을 하는 동안 레퍼런스 타입의 인수들은 레퍼런스가 아닌 타입으로 취급된다.
  • 유니버셜 레퍼런스에 대한 타입 추론을 할 때 lvalue 인수들은 특별한 취급을 받게 된다.
  • 매개변수들은 대한 타입 추론을 할 때 const 및 volatile 인수들은 non-const 및 non-volatile으로 취급된다.
  • 템플릿 타입 추론을 하는 동안, 배열 및 포인터를 사용하는 인수들은 초기화할 때 레퍼런스 타입을 사용하지 않으면 포인터로 붕괴된다.


 



- Jame Song

댓글