Item 2. auto의 타입 추론 규칙을 숙지해라

auto의 타입 추론은 template 방식과 매우 유사하다.


auto 타입 추론 규칙

template에서는 T와 ParamType을 나눠서 추론했다면 auto에서는 타입 지정자(type specifier)를 추론한다.

타입 지정자(type specifier)는 template의 ParamType 처럼 const 키워드나 &, && 같은 참조 한정사를 포함한 auto를 의미한다.

auto x = 27;        // x의 타입 지정자는 auto
const auto cx = x;  // cx의 타입 지정자는 const auto
const auto& rx = x; // rx의 타입 지정자는 const auto&

auto의 타입 추론은 타입 지정자의 종류에 따라 세 종류로 나뉘며 규칙은 template과 같다.

  1. 타입 지정자가 pointer 혹은 reference이지만 universal reference는 아닌 경우
  2. 타입 지정자가 universal reference인 경우
  3. 타입 지정자가 pointer 혹은 reference가 아닌 경우
// Case 1
int x = 27;
auto& rx = x;        // rx의 타입은 int&
const auto& crx = x; // crx의 타입은 const int&

// Case 2
auto&& uref1 = x;   // uref1의 타입은 int&
auto&& uref2 = crx; // uref2의 타입은 const int&
auto&& uref3 = 27;  // uref3의 타입은 int&&

// Case 3
auto vx = 10;       // vx의 타입은 int
const auto cx = vx; // cx의 타입은 const int

특히 타입 지정자가 pointer 혹은 reference도 아닌 경우, 타입 추론 과정에서 expression의 참조성(reference-ness)과 상수성(const-ness)이 무시하는 규칙을 명심해야 한다.

Range-based for loop를 사용하여 STL container를 순회할 때 auto& 대신 auto를 사용하면 실제 원소에 접근하지 않고 복사본을 생성하기 때문이다.

std::vector<std::string> vec{"1", "2", "3"};

// 실제 원소 값을 수정하지 못함
for (auto e : vec) {
  a = "0";
}
// "1", "2", "3"

// 실제 원소 값을 수정
for (auto& e : vec) {
  a = "0";
}
// "0", "0", "0"

비참조 타입 지정자의 경우, 타입 추론 과정에서 template처럼 배열과 함수가 포인터로 붕괴(decay)된다.

const char name[] = "heesu";

auto arr1 = name;  // arr1의 타입은 const char*
auto& arr2 = name; // arr2의 타입은 const char (&)[6]

void f(int, double);

auto f1 = f;  // f1의 타입은 void (*)(int, double)
auto& f2 = f; // f2의 타입은 void (&)(int, double)


Uniform Initialization 사용 시 발생하는 예외 상황

auto의 타입 추론 규칙에서 uniform initialization과 관련된 예외 상황이 존재한다.

먼저 uniform initialization과 initializer list은 다음과 같다.

균일한 초기화(Uniform Initialization)

C++11부터 생성자 호출과 함수 정의를 혼돈하지 않기 위해 ()(소괄호)대신 {}(중괄호)를 사용하는 uniform initialization을 지원한다.

// A 클래스 타입을 반환하는 a 함수 정의
A a();
// A 클래스 타입을 가지는 a 객체 생성
A a{};

초기화 리스트(Initializer List)

C++11부터 uniform initialization을 사용할 때 {}(중괄호) 안의 데이터를 인자로 받을 수 있는 std::initializer_list<T>가 추가되었다.

A(std::initializer_list<int> n) {}

A a = {1, 2, 3};


C++에서 변수를 초기화하는 방법은 4가지가 있다.

int x1 = 3;
int x2(3);
int x3 = {3};
int x4{3};

auto를 통해 타입을 추론할 때 uniform initialization을 사용할 경우 다음과 같은 차이가 존재한다.

auto x1 = 3;   // x1의 타입은 int
auto x2(3);    // x2의 타입은 int
auto x3 = {3}; // x3의 타입은 std::initializer_list<int>
auto x4{3};    // x4의 타입은 std::initializer_list<int>

auto x5 = { 1, 2, 3.0 }; // 컴파일 오류, std::initializer_list<T>의 T를 추론할 수 없음

auto는 uniform initialization을 사용했을 경우 std::initializer_list<T>로 추론한다.

std::initializer_list<T>template이기 때문에 여러 타입을 넣으면 컴파일러가 타입을 추론할 수 없다.

또한, auto 타입 추론에서는 braced initializer를 std::initializer_list<T>로 추론하지만 template 타입 추론에서는 braced initializer를 std::initializer_list<T>로 추론하지 못해 컴파일 오류가 발생한다.

template <typename T>
void f(T param);
 
f({ 11, 23, 9 }); // 템플릿은 std::initializer_list<T>를 추론하지 못함

만약 templatestd::initializer_list<T>를 추론하고 싶으면 ParamType에 직접 명시해줘야 한다.

template <typename T>
void f(std::initializer_list<T> param);
 
f({ 11, 23, 9 }); // T는 int, ParamType은 std::initializer_list<int>


함수의 리턴 타입이나 람다 매개변수에 auto 사용 시 발생하는 예외 상황

함수의 리턴 타입이나 람다 매개변수에 auto를 사용할 경우 auto의 타입 추론 규칙이 아닌 template의 타입 추론 규칙이 적용된다.

// 함수의 리턴 타입에 auto를 사용하는 경우
auto createInitList(void) {
  return { 1, 2, 3 };  // 템플릿의 타입 추론 규칙이 적용되어 {1, 2, 3}의 타입을 추론할 수 없음
}
 
// 람다 매개변수에 auto를 사용하는 경우
std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; };
reserV({ 1, 2, 3 }); // 템플릿의 타입 추론 규칙이 적용되어 {1, 2, 3}의 타입을 추론할 수 없음


카테고리:

업데이트: