프로그래밍 일반/C++ 프로그래밍

[Effective Modern C++] 항목 2: auto의 형식 연역 규칙에 숙지하라

지노윈 2020. 8. 18. 18:18
반응형

항목 1에서 템플릿 형식 연역을 알고 있다면 auto 형식 연역에 관해 알아야 할 것은 거의 알고 있는 셈입니다. 예외를 빼면 auto 형식 연역이 곧 템플릿 형식 연역이기 때문입니다. 

 

auto를 이용해서 변수를 선언할 때 auto는 템플릿의 T와 동일한 역할을 하며, 변수의 형식 지정자(type specifier)는 ParamType과 동일한 역할을 합니다. (T, ParamType이 무엇인지 모른다면 항목 1을 살펴봐주세요.) 

 

  auto x = 27; // x의 형식 지정자는 auto

  const auto cx = x;  // cx의 형식 지정자는 const auto

  const auto& rx = x; // rx의 형식 지정자는 const auto&

이 예들에서 x와 cx, rx의 형식들을 연역할 때, 컴파일러는 마치 선언마다 템플릿 함수 하나와 해당 초기화 표현식으로 그 템필릿 함수를 호출하는 구문이 존재하는 것처럼 행동한다. 즉,

 

  template<typename T>

  void func_for_x(T param)

  func_for_x(27);

 

  template<typename T>

  void func_for_cx(const T param);

  func_for_cx(x);

 

  template<typename T>

  void func_for_rx(const T& param);

  func_for_rx(x);

앞에서 말했듯이, auto에 대한 형식 연역은 예외 하나를 빼면 템플릿 형식 연역과 동일합니다.

auto 형식 연역 역시 세가지 경우로 나뉩니다.

 

  • 경우 1 : 형식 지정자가 포인터나 참조 형식이지만 Universal Reference는 아닌 경우
  • 경우 2 : 형식 지정자가 Universal Reference인 경우
  • 경우 3 : 형식 지정자가 포인터도 아니고 참조도 아닌 경우

경우 1과 3의 예는 앞에서 이미 보았습니다.

  auto x = 27; // 경우 3

  const auto cx = x;  // 경우 3

  const auto& rx = x; // 경우 1

경우 2도 기대한 대로 작동합니다.

  auto&& uref1 = x; // x는 int이자 lvalue이므로 uref1의 형식은 int&

  auto&& uref2 = cx;  // cx는 const int이자 lvalue이므로 uref2의 형식은 const int&

  auto&& uref3 = 27; // 27은 int이자 rvalue이므로 uref3의 형식은 int&&

 

항목 1에서 배열과 함수 이름이 포인터로 decay하는 방식을 논의 했었습니다. auto 형식 연역에서도 그러한 decay가 일어납니다.

  const char name[] = "R. N. Briggs";  // name의 형식은 const char[13]

  auto arr1 = name;   // arr1의 형식은 const char*

  auto& arr2 = name;  // arr2의 형식은 const char (&)[13];

  

  void someFunc(int, double);  // someFunc 함수의 형식은 void(int, double)

  auto func1 = someFunc;  // func1의 형식은 void (*)(int, double)

  auto& func2 = someFunc;  // func2의 형식은 void (&)(int, double)

이상의 예들에서 보듯이, auto의 형식 연역은 템플릿 형식 연역과 똑같이 작동합니다. 

 

auto와 템플릿이 다른점이 있는데 그 예를 살표봅시다.

  auto x1 = 27;  // 형식은 int, 값은 27

  auto x2(27);  // 형식은 int, 값은 27

  auto x3 = {27};  // 형식은 std::initializer_list<int>, 값은 {27}

auto로 선언된 변수의 초기치가 중괄호 쌍으로 감싸인 형태이면, 연역된 형식은 std::initializer_list이다. 만일 그런 형식을 연역할 수 없다면 컴파일이 거부됩니다.

  auto x5 = {1, 2, 3.0}; // 오류! 

이는 std::initializer_list<T> 연역되며 이것이 완성되려면 T의 형식을 연역해야 합니다. 그런데 T의 형식이 한 종류가 아니라서 연역에 실패합니다.

 

auto 형식 연역과 템플릿 형식 연역은 이처럼 중괄호 initializer가 관여할 때에만 차이를 보입니다.

  auto x = {11, 23, 9};  // x의 형식은 std::initializer_list<int>

 

  template<typename T>

  void f(T param);

  f({11, 23, 9});  // 오류! T에 대한 형식을 연역할 수 없음

 

하지만 param의 형식이 어떤 알려지지 않은 T에 대한 std::initializer_list<T>인 템플릿에 그 중괄호 초기치를 전달하면 템플릿 형식 연역 규칙들에 의해 T의 형식이 제대로 연역됩니다.

  template<typename T>

  void f(std::initializer_list<T> initList);

  f({11, 23, 9});  // T는 int로 연역되며, initList의 형식은 std::initializer_list<int>로 연역됩니다.

 

정리하면, auto 형식 연역과 템플릿 형식 연역의 차이는 std::initializer_list에 대한 한 가지 뿐입니다.

auto에서 중괄호 초기치는 std::initializer_list이지만 템플릿 형식 연역은 그렇지 않습니다.

 

C++14부터는 함수의 반환 형식을 auto로 지정해서 컴파일러가 연역하게 만들 수 있습니다. 람다의 매개 변수 선언에 auto를 사용하는 것도 가능합니다. 이러한 auto의 용법들에서는 auto 형식의 연역이 아니라 템플릿 형식 연역의 규칙들이 적용됩니다. 그래서 중괄호 초기치를 돌려주는 함수의 반환 형식을 auto로 지정하면 컴파일이 실패합니다.

auto createInitList()
{
	return {1, 2, 3};	// 오류! {1, 2, 3}의 형식을 연역 할 수 없습니다.
}

C++14 람다의 매개변수 형식 명세에 auto를 사용하는 경우에도 마찬가지 이유로 컴파일이 실패합니다.

std::vector<int> v;
...

auto resetV = 
	[&v](const auto& newValue) { v = newValue; };	// C++14
    
...
resetV({1, 2, 3});	// 오류! {1, 2, 3}의 형식을 연역할 수 없음

기억해 둘 사항

  • auto 형식 연역은 대체로 템플릿 형식 연역과 같지만, auto 형식 연역은 중괄호 초기치가 std::initializer_list를 나타낸다고 가정하는 반면 템플릿 형식 연역은 그렇지 않다는 차이가 있다.
  • 함수의 반환 형식이나 람다 매개변수으ㅔ 쓰이는 auto에 대해서는 auto 형식 연역이 아니라 템플릿 형식 연역이 적용된다.