條款二:理解`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