条款2:理解 auto 型别推导

ZaynPei Lv6

首先我们抛出这一讲的核心思想: auto 类型推导几乎就是模板类型推导, 除了稍后会介绍到的唯一区别之外

两者概念性的转换

这种对应关系可以通过一个概念性的转换来理解。例如, 对于前一讲介绍的函数模板调用:

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr); // 编译器利用 expr 推导 T 和 ParamType

一个使用auto声明的变量可以被看作是这个模式的变体,其中auto扮演了模板中的T,而变量的类型修饰符(如const、&等)和T一起则扮演了ParamType

例如,以下auto声明:

1
2
3
auto x = 27;
const auto cx = x;
const auto& rx = x;
在概念上,编译器为了推导它们的类型,其行为就如同为每个声明生成了一个模板并用初始化物调用它一样 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 概念上为推导 x 的类型可以理解为存在以下模板
template<typename T>
void func_for_x(T param);
func_for_x(27); // param 的类型就是 x 的类型

// 概念上为推导 cx 的类型
template<typename T>
void func_for_cx(const T param);
func_for_cx(x); // param 的类型就是 cx 的类型

// 概念上为推导 rx 的类型
template<typename T>
void func_for_rx(const T& param);
func_for_rx(x); // param 的类型就是 rx 的类型

auto的三种推导情况

因为auto类型推导与模板类型推导机制相同,条款1中划分的三种推导情况也完全适用于auto。

  1. 情况1:类型修饰符是指针或引用(但非万能引用)

此时推导规则与模板相同,初始化表达式的引用性和const属性会被保留。

1
const auto& rx = x; // rx 是一个非万能引用 [cite: 278]
  1. 情况2:类型修饰符是万能引用

当使用auto&&时,左值和右值初始化物会被区别对待,推导出不同的类型。

1
2
auto&& uref1 = x;  // x 是 int 且是左值, uref1 类型为 int& [cite: 279]
auto&& uref3 = 27; // 27 是 int 且是右值, uref3 类型为 int&& [cite: 279]
  1. 情况3:类型修饰符既非指针也非引用

此时推导规则也与模板相同,初始化表达式的引用性、const和volatile属性都会被忽略。

1
2
auto x = 27; // 情况3 (x既非指针也非引用) [cite: 276]
const auto cx = x; // cx 也属于情况3 [cite: 277]

同样地,数组和函数名在auto类型推导中也会退化成指针,除非auto被声明为引用。

1
2
3
4
5
6
7
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类型推导和模板类型推导真正的唯一区别在于它们如何处理用大括号括起来的初始化表达式。

正如在条款7中提到的, 在C++11中,有多种语法可以初始化一个int:

1
2
3
4
int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};

当把int替换为auto时,前两种写法的行为符合预期,变量被推导为int。然而,后两种使用大括号的写法,会触发一条针对auto的特殊推导规则:当用于auto声明的变量的初始化表达式是用大括号括起时,推导所得的类型就是std::initializer_list

1
2
3
4
5
auto x1 = 27;   // 类型是 int [cite: 291]
auto x2(27); // 类型是 int [cite: 293]

auto x3 = {27}; // 类型是 std::initializer_list<int> [cite: 293]
auto x4{27}; // 类型是 std::initializer_list<int> [cite: 294]

这个特殊规则是auto独有的。如果将同样的大括号初始化物传递给一个函数模板,类型推导会失败,代码将无法通过编译; 而auto则可以

1
2
3
4
5
6
auto x = {11, 23, 9}; // x 的类型是 std::initializer_list<int> [cite: 300]

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

f({11, 23, 9}); // 错误!无法为 T 推导出类型
#### 在lambda表达式中的特殊

不过需要注意的是,这条关于大括号的特殊规则仅适用于auto变量声明。在C++14中,当auto被用于推导函数返回值或用于lambda表达式的形参时,它遵循的是模板类型推导的规则,而非auto的特殊规则 。

因此,一个返回大括号初始化表达式的函数将无法通过编译,因为它遵循的是模板类型推导,而模板类型推导无法处理这种情况 。

1
2
3
4
5
6
7
8
9
10
// C++14
auto createInitList() {
return {1, 2, 3}; // 错误!无法为 {1, 2, 3} 完成类型推导
}

std:: vector<int> v;
auto resetV =
[&v ](const auto& newValue) { v = newValue; } ; // (++14 合法)

resetV({ 1, 2, 3 }) ; // 错误!无法为{ 1, 2, 3} 完成型别推导