条款二:理解`auto`类型推导
=========================
如果你已经阅读了条款1关于模板相关的类型推导,你就已经知道了机会所有关于`auto`的类型推导,因为除了一个例外,`auto`类型推导就是模板类型推导。但是它怎么就会是模板类型推导呢?模板类型推导涉及模板和函数以及参数,但是`auto`和上面的这些没有任何的关系。
这是对的,但是没有关系。模板类型推导和`auto`类型推导是有一个直接的映射。有一个书面上的从一种情况转换成另外一种情况的算法。
在条款1,模板类型推导是使用下面的通用模板函数来解释的:
```cpp
template<typename T>
void f(ParamType param);
```
在这里通常调用:
```cpp
f(expr); // 使用一些表达式来当做调用f的参数
```
在调用`f`的地方,编译器使用`expr`来推导`T`和`ParamType`的类型。
当一个变量被声明为`auto`,`auto`相当于模板中的`T`,而对变量做的相关的类型限定就像`ParamType`。这用代码说明比直接解释更加容易理解,所以看下面的这个例子:
```cpp
auto x = 27;
```
这里,对`x`的类型定义就仅仅是`auto`本身。从另一方面,在这个声明中:
```cpp
const auto cx = x;
```
类型被声明成`const auto`,在这儿:
```cpp
const auto& rx = x;
```
类型被声明称`const auto&`。在这些例子中推导`x`,`cx`,`rx`的类型的时候,编译器处理每个声明的时候就和处理对应的表达式初始化的模板:
```cpp
template<typename T> // 推导x的类型的
void func_for_x(T param); // 概念上的模板
func_for_x(27); // 概念上的调用:
// param的类型就是x的类型
template<typename T>
void func_for_cx(const T param); // 推导cx的概念上的模板
func_for_cx(x); // 概念调用:param的推导类型就是cx的类型
template<typename T>
void func_for_rx(const T& param); // 推导rx概念上的模板
func_for_rx(x); // 概念调用:param的推导类型就是rx的类型
```
正如我所说,对`auto`的类型推导只存在一种情况的例外(这个后面就会讨论),其他的就和模板类型推导完全一样了。
条款1把模板类型推导划分成三部分,基于在通用的函数模板的`ParamType`的特性和`param`的类型声明。在一个用`auto`声明的变量上,类型声明代替了`ParamType`的作用,所以也有三种情况:
* 情况1:类型声明是一个指针或者是一个引用,但不是一个通用的引用
* 情况2:类型声明是一个通用引用
* 情况3:类型声明既不是一个指针也不是一个引用
我们已经看了情况1和情况3的例子:
```cpp
auto x = 27; // 情况3(x既不是指针也不是引用)
const auto cx = x; // 情况3(cx二者都不是)
const auto& rx = x; // 情况1(rx是一个非通用的引用)
```
情况2正如你期待的那样:
```cpp
auto&& uref1 = x; // x是int并且是左值
// 所以uref1的类型是int&
auto&& uref2 = cx; // cx是int并且是左值
// 所以uref2的类型是const int&
auto&& uref3 = 27; // 27是int并且是右值
// 所以uref3的类型是int&&
```
条款1讲解了在非引用类型声明里,数组和函数名称如何退化成指针。这在`auto`类型推导上面也是一样:
```cpp
const char name[] = // name的类型是const char[13]
"R. N. Briggs";
auto arr1 = name; // arr1的类型是const char*
auto& arr2 = name; // arr2的类型是const char (&)[13]
void someFunc(int, double); // someFunc是一个函数,类型是
// void (*)(int, double)
auto& func2 = someFunc; // func1的类型是
// void (&)(int, double)
```
正如你所见,`auto`类型推导和模板类型推导工作很类似。它们就像一枚硬币的两面。
除了有一种情况是不一样的。我们从如果你想声明一个用27初始化的`int`, C++98你有两种语法选择:
```cpp
int x1 = 27;
int x2(27);
```
C++11,通过标准支持的统一初始化(使用花括号初始化——译者注),可以添加下面的代码:
```cpp
int x3 = { 27 };
int x4{ 27 };
```
综上四种语法,都会生成一种结果:一个拥有27数值的`int`。
但是正如条款5所解释的,使用`auto`来声明变量比使用固定的类型更好,所以在上述的声明中把`int`换成`auto`更好。最直白的写法就如下面的代码:
```cpp
auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{ 27 };
```
上面的所有声明都可以编译,但是他们和被替换的相对应的语句的意义并不一样。头两个的确是一样的,声明一个初始化值为27的`int`。然而后面两个,声明了一个类型为`std::intializer_list<int>`的变量,这个变量包含了一个单一的元素27!
```cpp
auto x1 = 27; // 类型时int,值是27
auto x2(27); // 同上
auto x3 = { 27 }; // 类型是std::intializer_list<int>
// 值是{ 27 }
auto x4{ 27 }; // 同上
```
这和`auto`的一种特殊类型推导有关系。当使用一对花括号来初始化一个`auto`类型的变量的时候,推导的类型是`std::intializer_list`。如果这种类型无法被推导(比如在花括号中的变量拥有不同的类型),代码会编译错误。
```cpp
auto x5 = { 1, 2, 3.0 }; // 错误! 不能讲T推导成
// std::intializer_list<T>
```
正如注释中所说的,在这种情况,类型推导会失败,但是认识到这里实际上是有两种类型推导是非常重要的。一种是`auto: x5`的类型被推导。因为`x5`的初始化是在花括号里面,`x5`必须被推导成`std::intializer_list`。但是`std::intializer_list`是一个模板。实例是对一些`T`实例化成`std::intializer_list<T>`,这就意味着`T`的类型必须被推导出来。类型推导就在第二种的推导的范围上失败了。在这个例子中,类型推导失败是因为在花括号里面的数值并不是单一类型的。
对待花括号初始化的行为是`auto`唯一和模板类型推导不一样的地方。当`auto`声明变量被使用一对花括号初始化,推导的类型是`std::intializer_list`的一个实例。但是如果相同的初始化递给相同的模板,类型推导会失败,代码不能编译。
```cpp
auto x = { 11, 23, 9 }; // x的类型是
// std::initializer_list<int>
template<typename T> // 和x的声明等价的
void f(T param); // 模板
f({ 11, 23, 9 }); // 错误的!没办法推导T的类型
```
但是,如果你明确模板的`param`的类型是一个不知道`T`类型的`std::initializer_list<T>`:
```cpp
template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); // T被推导成int,initList的
// 类型是std::initializer_list<int>
```
所以`auto`和模板类型推导的本质区别就是`auto`假设花括号初始化代表的是std::initializer_list,但是模板类型推导却不是。
你可能对为什么`auto`类型推导有一个对花括号初始化有一个特殊的规则而模板的类型推导却没有感兴趣。我自己也非常奇怪。可是我一直没有能够找到一个有力的解释。但是法则就是法则,这就意味着你必须记住如果使用`auto`声明一个变量并且使用花括号来初始化它,类型推导的就是`std::initializer_list`。你必须习惯这种花括号的初始化哲学——使用花括号里面的数值来初始化是理所当然的。在C++11编程里面的一个经典的错误就是误被声明成`std::initializer_list`,而其实你是想声明另外的一种类型。这个陷阱使得一些开发者仅仅在必要的时候才会在初始化数值周围加上花括号。(什么时候是必要的会在条款7里面讨论。)
对于C++11,这是一个完整的故事,但是对于C++14来说,故事还要继续。C++14允许`auto`表示推导的函数返回值(参看条款3),而且C++14的lambda可能会在参数声明里面使用`auto`。但是,这里面的使用是复用了模板的类型推导,而不是`auto`的类型推导。所以一个使用`auto`声明的返回值的函数,返回一个花括号初始化就无法编译。
```cpp
auto createInitList()
{
return { 1, 2, 3 }; // 编译错误:不能推导出{ 1, 2, 3 }的类型
}
```
在C++14的lambda里面,当`auto`用在参数类型声明的时候也是如此:
```cpp
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`在函数返回值或者lambda参数里面执行模板的类型推导,而不是通常意义的`auto`类型推导|
- 出版者的忠告
- 致谢
- 简介
- 第一章 类型推导
- 条款1:理解模板类型推导
- 条款2:理解auto类型推导
- 条款3:理解decltype
- 条款4:知道如何查看类型推导
- 第二章 auto关键字
- 条款5:优先使用auto而非显式类型声明
- 条款6:当auto推导出非预期类型时应当使用显式的类型初始化
- 第三章 使用现代C++
- 条款7:创建对象时区分()和{}
- 条款8:优先使用nullptr而不是0或者NULL
- 条款9:优先使用声明别名而不是typedef
- 条款10:优先使用作用域限制的enmu而不是无作用域的enum
- 条款11:优先使用delete关键字删除函数而不是private却又不实现的函数
- 条款12:使用override关键字声明覆盖的函数
- 条款13:优先使用const_iterator而不是iterator
- 条款14:使用noexcept修饰不想抛出异常的函数
- 条款15:尽可能的使用constexpr
- 条款16:保证const成员函数线程安全
- 条款17:理解特殊成员函数的生成
- 第四章 智能指针
- 条款18:使用std::unique_ptr管理独占资源
- 条款19:使用std::shared_ptr管理共享资源
- 条款20:在std::shared_ptr类似指针可以悬挂时使用std::weak_ptr
- 条款21:优先使用std::make_unique和std::make_shared而不是直接使用new
- 条款22:当使用Pimpl的时候在实现文件中定义特殊的成员函数
- 第五章 右值引用、移动语义和完美转发
- 条款23:理解std::move和std::forward
- 条款24:区分通用引用和右值引用
- 条款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 条款26:避免在通用引用上重定义函数
- 条款27:熟悉通用引用上重定义函数的其他选择
- 条款28:理解引用折叠
- 条款29:假定移动操作不存在,不廉价,不使用
- 条款30:熟悉完美转发和失败的情况
- 第六章 Lambda表达式
- 条款31:避免默认的参数捕捉
- 条款32:使用init捕捉来移动对象到闭包
- 条款33:在auto&&参数上使用decltype当std::forward auto&&参数
- 条款34:优先使用lambda而不是std::bind
- 第七章 并发API
- 条款35:优先使用task-based而不是thread-based
- 条款36:当异步是必要的时声明std::launch::async
- 条款37:使得std::thread在所有的路径下无法join
- 条款38:注意线程句柄析构的行为
- 条款39:考虑在一次性事件通信上void的特性
- 条款40:在并发时使用std::atomic 在特殊内存上使用volatile
- 第八章 改进
- 条款41:考虑对拷贝参数按值传递移动廉价,那就尽量拷贝
- 条款42:考虑使用emplace代替insert