[EMC++] Item 2. auto의 타입 추론 규칙을 숙지해라
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
과 같다.
- 타입 지정자가 pointer 혹은 reference이지만 universal reference는 아닌 경우
- 타입 지정자가 universal reference인 경우
- 타입 지정자가 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>를 추론하지 못함
만약 template
이 std::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}의 타입을 추론할 수 없음