Advanced C++

[Modern C++] (1) 형식 연역

로파이 2021. 3. 19. 22:48

템플릿 함수 및 클래스와 C++11/14 혹은 그 이상의 버전에서 사용되는 auto 키워드에서 추론되는 형식 연역을 정리한다.

 

템플릿 형식 연역

 

템플릿 선언

template<typename T> // T에 대한 형식 연역

void f(ParamType param); // 전달된 param에 대한 형식 ParamType

 

호출 방식

f(expr); // expr 이라는 표현식을 사용

 

경우 1. ParamType이 포인터 혹은 참조 형식인 경우

단순 참조 형식 T&

template<typename T>
void f(T& param);
변수 선언 함수 호출 연역된 T 형식  param의 형식
int x = 27; f(x); int int &
const int cx = x; f(cx); const int const int &
const int& rx = x; f(rx); const int const int &

- 특징

함수의 매개변수에서 "ParamType = T&" 이기 때문에,

rx 변수와 같은 const int& 기존 참조 형식에서 참조 형식&을 무시하고 T = const int로 연역된다. 

T에 대한 패턴 매칭 방식

 

상수 참조 형식 const T&

template<typename T>
void f(const T& param);
변수 선언 함수 호출 연역된 T 형식  param의 형식
int x = 27; f(x); int const int &
const int cx = x; f(cx); int const int &
const int& rx = x; f(rx); int const int &

포인터 T*

template<typename T>
void f(T* param);
변수 선언 함수 호출 연역된 T 형식  param의 형식
int x = 27; f(&x); int int *
int* cx = &x; f(cx); int int *
const int* rx = cx; f(rx); const int const int*

포인터에 대한 연역도 참조 형식과 같다.

 

- 특징

T에 대한 패턴 매칭 방식으로 인해 T를 연역시 참조 형식도 무시되고 const 형식도 무시된다.

 

경우 2. ParamType이 보편 참조인 경우

보편 참조란 모든 ParamType에 대해 보편적으로 참조형으로 만든다는 명칭이다.

template<typename T>
void f(T&& param);
변수 선언 함수 호출 연역된 T 형식  param의 형식
int x = 27; f(x); int & int &
const int cx = x; f(cx); const int & const int &
const int& rx = x; f(rx); const int & const int &
  f(27); int int &&

이름 있는 변수, lvalue(x, cx, rx)는 모두 참조 형식으로 연역된다.

rvalue는 패턴 매칭이 적용되고 T는 && 형식을 제외되어 연역된다.

보편 참조에서 왼값에는 패턴 매칭이 적용되지 않는다.

 

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

pass-by-value로 함수 매개변수가 전달된 상황이다.

template<typename T>
void f(T param);
변수 선언 함수 호출 연역된 T 형식  param의 형식
int x = 27; f(x); int int
const int cx = x; f(cx); int int
const int& rx = x; f(rx); int int
const char* const str = "name"; f(str); const char* const char*

참조 형식일 경우 참조 형식은 무시, const 도 무시된다.

단순 값이 복사되어 전달되기 때문에 새로운 객체로 인식한다.

상수 값을 가리키는 상수 포인터의 경우 상수 값은 유지되고 포인터에 대한 상수성만 잃게된다.

 

배열 인수 / 함수 인수

포인터로 붕괴하는 현상

 

값 전달 방식의 템플릿 인수

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

char name[10] = "메뚜기";

f(name); // name은 char [10]의 배열이지만 T = char* 포인터로 연역(붕괴)된다.

 

void Foo(int, double);

f(Foo); // T는 void (*)(int double) 함수 포인터로 연역된다.

 

참조 형식의 템플릿 인수

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

 

char name[10] = "메뚜기";

f(name); // T는 여기서 char [10]로 연역되고 param 타입은 char (&)[10] 이다.

참조 형식의 경우 배열은 실제 형식으로 연역된다.

 

void Foo(int, double);

f(Foo); // T는 void (&)(int double) 함수 참조로 연역된다.

 

auto 형식 연역

 

기본적으로 템플릿 형식 연역과 같다.

 

경우 1. 형식 지정자가 포인터나 참조 형식 인 경우 (보편 참조 x)

경우 2. 형식 지정자가 보편 참조 인 경우

경우 3. 형식 지정자가 포인터도 아니고 참조도 아닌 경우

auto x = 12;        // int (경우 3)
const auto cx = x;  // int (경우 3)
const auto& rx = x; // int (경우 1)
/*  경우 2    */
auto&& uref1 = x;   // x는 lvalue, uref1 형식은 int&
auto&& uref2 = cx;  // cx는 lvalue, uref2 형식은 const int&
auto&& uref3 = 27;  // 27은 rvalue, uref3 형식은 int

포인터 붕괴외 참조 형식

const char name[] = "Arck"; // const char [5]
auto arr1 = name // arr1는 const char* 포인터
auto& arr2 = name // arr2는 const char (&)[5] 참조 형식의 배열

void Foo(int, double); // void(int, double) 형식의 함수
auto func1 = Foo;       // func1는 void(*)(int, double) 함수 포인터
auto& func2 = Foo;     // func2는 void(&)(int, double) 함수 참조

 

차이점 : std::initializer_list<T>

중괄호를 사용한 초기화의 경우 auto의 연역 방식에 std::initializer_list<T>가 추가된다.

auto x(10);   // x는 int
auto x2 = {10}; // x는 std::initializer_list<int>
auto arr = {10, 20, 30, 40.0}; // 중괄호안의 변수 형식이 하나라도 다른 경우 연역 불가 (컴파일 에러)

 

템플릿 인수로 std::initializer_list<T>형식의 매개변수를 전달할 경우 연역에 실패한다.

template<typename T>

void func(T param);

 

func({1,2,3,4}); // 연역 실패

 

따라서 다음과 같이 std::initializer_list<T>를 인수로 명시해야 연역 가능하다.

template<typename T>

void func(std::initializer_list<T> param);

 

func({1,2,3,4}); // 연역 성공, T = int (패턴 매칭)

 

- C++14의 auto 키워드를 반환 형식으로 사용

기본적으로 함수 반환의 auto는 템플릿 연역 방식을 사용한다.

auto createInitList()

{

   return {1,2,3,4};  // 연역 실패

}

auto createInitList()
{
 return {1,2,3,4}; // 연역 실패
}

람다 함수내의 매개변수에 auto를 지정하는 경우도 마찬가지이다.

 

vector<int> vec;

const auto change_key = [&vec](const auto& new_arr) { vec = new_arr; }

change_key({5,4,3,2,1}); // 연역 실패

vector<int> vec;
const auto change_key = [&vec](const auto& new_arr) { vec = new_arr; };
change_key({5,4,3,2,1}); // 연역 실패

 

decltype

 

변수의 실제 형식을 그대로 말해준다.

int x = 10;
decltype(x) y = 2; // x는 int
vector<float> v = {1.2f, 3.2f};
decltype(v) f = {3.0f}; // f는 vector<float>

 

사용 예시

- 매개변수 타입을 따르는 반환 형식을 가진 함수 정의

template<typename Container, typename Index>
auto AuthorizeAccess(Container& c, Index i)
			-> decltype(c[i])
{
  authenticateUser();
  return c[i];
}

C++ 11의 후행 반환 형식 ->은 이 뒤에 나올 형식이 실제 반환 타입을 따르는 것을 명시한다.

c와 i가 먼저 선언되어야 하기 때문에 후행에 선언된다.

 

만약 decltype을 사용하지 않는 다면 auto는 템플릿 연역법을 사용하므로 다음 예제에서 다음과 같이 연역되므로

auto = int가 되어 실제 c[i]의 타입 int& 참조가 아니여서 값이 변경되지 않는다.

template<typename Container, typename Index>
auto AuthorizeAccess(Container& c, Index i)
{
  authenticateUser();
  return c[i];
}

vector<int> data = {2,4,2,1,3};
AuthorizeAccess(data, 10) = 7; // 값 변경되지 않음

- C++ 14 형식의 정의

template<typename Container, typename Index>
decltype(auto) AuthorizeAccess(Container& c, Index i)
{
  authenticateUser();
  return c[i];
}

decltype(auto)를 사용하면 실제 반환값과 같은 형식으로 연역된다.

 

- decltype(x)는 decltype((x))와 다르다.

 

int x = 10;

decltype(x) = int;

decltype((x)) = int&;

 

연역된 형식을 파악하는 방법 typeinfo

 

decltype(x)는 함수가 아니기 때문에 사용자가 실제로 어떤 형식을 가리키는 지 알기가 어렵다.

typeinfo(x)는 x 변수에 대해 타입 정보를 반환하고 

typeinfo(x).name()을 이용하면 형식에 대한 문자열을 반환하여 이 내용을 파악할 수 있다.

typeinfo는 함수이기 때문에 매개변수가 pass-by-value로 전달되어 템플릿 함수 내에서 T와 param 타입이 같게 나오는 경우가 있다.

template<typename T>
void f(const T& param)
{
	using std::cout;
    
    // T를 표시
    cout << "T = "
    	<< typeinfo(T).name()  // class const Widget *
        << "\n";
        
    // param를 표시
    cout << "param = "
    	<< typeinfo(param).name()  // class const Widget *
        << "\n";
}

int main()
{
	Widget inst = Widget;
    
	f(&inst);
    
	return 0;
}

const Widget * const & 타입 param 변수가 typeinfo(ptr) 내부에서 const Widget * ptr로 변형됨. 

 

Boost의 type_index 라이브러리

 

boost::typeindex::type_id_with_cvr를 이용하여 정확한 템플릿 인자 T에 대한 연역정보를 얻을 수 있다.

#include <boost/type_index.hpp>

template<typename T>
void f(const T& param)
{
	using std::cout;
	using boost::typeindex::type_id_with_cvr;
    
    // T를 표시
    cout << "T = "
    	<< type_id_with_cvr<T>().pretty_name() // T = class Widget const*
        << "\n";
        
    // param를 표시
    cout << "param = "
    	<< type_id_with_cvr<decltype(param)>().pretty_name() // param = class Widget const* const&
        << "\n";
}
int main()
{

  Widget wgt = Widget;

  f(&wgt);
           
}

 

참고: Effective Modern C++