條款五:優先使用`auto`而非顯式類型聲明
=========================
使用下面語句是簡單快樂的
```cpp
int x;
```
等等。見鬼,我忘記初始化`x`了,因此它的值是無法確定的。也許,它會被初始化為0。但是這根據上下文語境決定。這真令人嘆息。
不要介意。我們來看看一個要通過迭代器解引用初始化的局部變量聲明的簡單與快樂。
```cpp
template<typename It>
void dwim(It b, It e)
{
while(b != e){
typename std::iterator_traits<It>::value_type
currValue = *b;
...
}
}
```
額。`typename std::iterator_traits<It>::value_type`來表示被迭代器指向的值的類型?真的是這樣嗎?我必須努力不去想這是多么有趣的一件事。見鬼。等等,難道我已經說出來了。
好吧,有三個令人愉悅的地方:聲明一個封裝好的局部變量的類型帶來的快樂。是的,這是沒有問題的。一個封裝體的類型只有編譯器知道,因此不能被顯示的寫出來。哎,見鬼。
見鬼,見鬼,見鬼!使用`C++`編程并不是它本該有的愉悅體驗。
是的,過去的確不是。但是由于`C++11`,得益于`auto`,這些問題都消失了。`auto`變量從他們的初始化推導出其類型,所以它們必須被初始化。這就意味著你可以在現代的`C++`高速公路上對沒有初始化的變量的問題說再見了。
``` cpp
int x1; // potentially uninitialized
auto x2; // error! initializer required
auto x3 = 0; // fine, x's value is well-defined
```
如上所述,高速公路上不再有由于解引用迭代器的聲明局部變量而引起的坑坑洼洼。
``` cpp
template<typename It>
void dwim(It b, It e)
{
while(b != e){
auto currValue = *b;
...
}
}
```
由于`auto`使用類型推導(參見條款2),它可以表示那些僅僅被編譯器知曉的類型:
``` cpp
auto dereUPLess = // comparison func.
[](const std::unique_ptr<Widget>& p1, // for Widgets
const std::unique_ptr<Widget>& p2) // pointed to by
{ return *p1 < *p2}; // std::unique_ptrs
```
非常酷。在`C++14`中,模板(原文為temperature)被進一步丟棄,因為使用`lambda`表達式的參數可以包含`auto`:
``` cpp
auto derefLess = // C++14 comparison
[](const auto& p1, // function for
const auto& p2) // values pointed
{ return *p1 < *p2; };
```
盡管非常酷,也許你在想,我們不需要使用`auto`去聲明一個持有封裝體的變量,因為我們可以使用一個`std::function`對象。這是千真萬確的,我們可以這樣干,但是也許那不是你正在思考的東西。也許你在思考“`std::function`是什么東東?”。因此讓我們解釋清楚。
`std::function`是`C++11`標準庫的一個模板,它可以使函數指針普通化。鑒于函數指針只能指向一個函數,然而,`std::function`對象可以應用任何可以被調用的對象,就像函數。就像你聲明一個函數指針的時候,必須指明這個函數指針指向的函數的類型,你產生一個`std::function`對象時,你也指明它要引用的函數的類型。你可以通過`std::function`的模板參數來完成這個工作。例如,有聲明一個名為`func`的`std::function`對象,它可以引用有如下特點的可調用對象:
``` cpp
bool(const std::unique_ptr<Widget> &, // C++11 signature for
const std::unique_ptr<Widget> &) // std::unique_ptr<Widget>
// comparison funtion
```
你可以這么寫:
``` cpp
std::function<bool(const std::unique_ptr<Widget> &,
const std::unique_ptr<Widget> &)> func;
```
因為`lambda`表達式得到一個可調用對象,封裝體可以存儲在`std::function`對象里面。這意味著,我們可以聲明不適用`auto`的`C++11`版本的`dereUPLess`如下:
``` cpp
std::function<bool(const std::unique_ptr<Widget>&,
const std::unique_ptr<Widget>&)>
derefUPLess = [](const std::unique_ptr<Widget>& p1,
const std::unique_ptr<Widget>& p2)
{return *p1 < *p2; };
```
意識到需要重復參數的類型這種冗余的語法是重要的,使用`std::function`和使用`auto`并不一樣。一個使用`auto`聲明持有一個封裝的變量和封裝體有同樣的類型,也僅使用和封裝體同樣大小的內存。持有一個封裝體的被`std::function`聲明的變量的類型是`std::function`模板的一個實例,并且對任何類型只有一個固定的大小。這個內存大小可能不能滿足封裝體的需求。出現這種情況時,`std::function`將會開辟堆空間來存儲這個封裝體。導致的結果就是`std::function`對象一般會比`auto`聲明的對象使用更多的內存。由于實現細節中,約束內嵌的使用和提供間接函數的調用,通過`std::function`對象來調用一個封裝體比通過`auto`對象要慢。換言之,`std::function`方法通常體積比`auto`大,并且慢,還有可能導致內存不足的異常。就像你在上面一個例子中看到的,使用`auto`的工作量明顯小于使用`std::function`。持有一個封裝體時,`auto`和`std::function`之間的競爭,對`auto`簡直就是游戲。(一個相似的論點也成立對于持有`std::blind`調用結果的`auto`和`std::function`,但是在條款34中,我將竭盡所能的說服你盡可能使用`lambda`表達式,而不是`std::blind`)。
`auto`的優點除了可以避免未初始化的變量,變量聲明引起的歧義,直接持有封裝體的能力。還有一個就是可以避免“類型截斷”問題。下面有個例子,你可能見過或者寫過:
```cpp
std::vector<int> v;
...
unsigned sz = v.size();
```
`v.size()`定義的返回類型是`std::vector<int>::size_type`,但是很少有開發者對此十分清楚。`std::vector<int>::size_type`被指定為一個非符號的整數類型,因此很多程序員認為`unsigned`類型是足夠的,然后寫出了上面的代碼。這將導致一些有趣的后果。比如說在32位`Windows`系統上,`unsigned`和`std::vector<int>::size_type`有同樣的大小,但是在64位的`Windows`上,`unsigned`是32bit的,而`std::vector<int>::size_type`是64bit的。這意味著上面的代碼在32位`Windows`系統上工作良好,但是在64位`Windows`系統上時有可能不正確,當應用程序從32位移植到64位上時,誰又想在這種問題上浪費時間呢?
使用`auto`可以保證你不必被上面的東西所困擾:
```cpp
auto sz = v.size() // sz's type is std::vector<int>::size_type
```
仍然不太確定使用`auto`的高明之處?看看下面的代碼:
```cpp
std::unordered_map<std::string, int> m;
...
for (const std::pair<std::string, int>& p : m)
{
... // do something with p
}
```
這看上去完美合理。但是有一個問題,你看出來了嗎?
意識到`std::unorder_map`的`key`部分是`const`類型的,在哈希表中的`std::pair`的類型不是`std::pair<std::string, int>`,而是`std::pair<const std::sting, int>`。但是這不是循環體外變量`p`的聲明類型。后果就是,編譯器竭盡全力去找到一種方式,把`std::pair<const std::string, int>`對象(正是哈希表中的內容)轉化為`std::pair<std::string, int>`對象(`p`的聲明類型)。這個過程將通過復制`m`的一個元素到一個臨時對象,然后將這個臨時對象和`p`綁定完成。在每個循環結束的時候這個臨時對象將被銷毀。如果是你寫了這個循環,你將會感覺代碼的行為令人吃驚,因為你本來想簡單地將引用`p`和`m`的每個元素綁定的。
這種無意的類型不匹配可以通過`auto`解決
```cpp
for (const auto& p : m)
{
... // as before
}
```
這不僅僅更高效,也更容易敲擊代碼。更近一步,這個代碼還有一些吸引人的特性,比如如果你要取`p`的地址,你的確得到一個指向`m`的元素的指針。如果不使用`auto`,你將得到一個指向臨時對象的指針——這個臨時對象在每次循環結束時將被銷毀。
上面兩個例子中——在應該使用`std::vector<int>::size_type`的時候使用`unsigned`和在該使用`std::pair<const std::sting, int>`的地方使用`std::pair<std::string, int>`——說明顯式指定的類型是如何導致你萬萬沒想到的隱式的轉換的。如果你使用`auto`作為目標變量的類型,你不必為你聲明類型和用來初始化它的表達式類型之間的不匹配而擔心。
有好幾個使用`auto`而不是顯式類型聲明的原因。然而,`auto`不是完美的。`auto`變量的類型都是從初始化它的表達式推導出來的,一些初始化表達式并不是我們期望的類型。發生這種情況時,你可以參考條款2和條款6來決定怎么辦,我不在此處展開了。相反,我將我的精力集中在你將傳統的類型聲明替代為`auto`時帶來的代碼可讀性問題。
首先,深呼吸放松一下。`auto`是一個可選項,不是必須項。如果根據你的專業判斷,使用顯式的類型聲明比使用`auto`會使你的代碼更加清晰或者更好維護,或者在其他方面更有優勢,你可以繼續使用顯式的類型聲明。牢記一點,`C++`并沒有在這個方面有什么大的突破,這種技術在其他語言中被熟知,叫做類型推斷(`type inference`)。其他的靜態類型過程式語言(像`C#`,`D`,`Scala`,`Visual Basic`)也有或多或少等價的特點,對靜態類型的函數編程語言(像`ML`,`Haskell`,`OCaml`,`F#`等)另當別論。一定程度上說,這是受到動態類型語言的成功所啟發,比如`Perl`,`Python`,`Ruby`,在這些語言中很少顯式指定變量的類型。軟件開發社區對于類型推斷有很豐富的經驗,這些經驗表明這些技術和創建及維護巨大的工業級代碼庫沒有矛盾。
一些開發者被這樣的事實困擾,使用`auto`會消除看一眼源代碼就能確定對象的類型的能力。然而,IDE提示對象類型的功能經常能緩解這個問題(甚至考慮到在條款4中提到的IDE的類型顯示問題),在很多情況下,一個對象類型的摘要視圖和顯示完全的類型一樣有用。比如,摘要視圖足以讓開發者知道這個對象是容器還是計數器或者一個智能指針,而不需要知道這個容器,計數器或者智能指針的確切特性。假設比較好的選擇變量名字,這樣的摘要類型信息幾乎總是唾手可得的。
事實是顯式地寫出類型可能會引入一些難以察覺的錯誤,導致正確性或者效率問題,或者兩者兼而有之。除此之外,`auto`類型會自動的改變如果初始化它的表達式改變后,這意味著通過使用`auto`可以使代碼重構變得更簡單。舉個例子,如果一個函數被聲明為返回`int`,但是你稍后決定返回`long`可能更好一些,如果你把這個函數的返回結果存儲在一個`auto`變量中,在下次編譯的時候,調用代碼將會自動的更新。結果如果存儲在一個顯式聲明為`int`的變量中,你需要找到所有調用這個函數的地方然后改寫他們。
|要記住的東西|
| :--------- |
| `auto`變量一定要被初始化,并且對由于類型不匹配引起的兼容和效率問題有免疫力,可以簡單化代碼重構,一般會比顯式的聲明類型敲擊更少的鍵盤|
| `auto`類型的變量也受限于[條款2](../DeducingTypes/2-Understand-auto-type-deduction.html)和條款6中描述的陷阱|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款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