條款6:當auto推導出非預期類型時應當使用顯式的類型初始化
===============================================
條款5解釋了使用`auto`關鍵字去聲明變量,這樣就比直接顯示聲明類型提供了一系列的技術優勢,但是有時候`auto`的類型推導會和你想的南轅北轍。舉一個例子,假設我有一個函數接受一個`Widget`返回一個`std::vector<bool>`,其中每個`bool`表征`Widget`是否接受一個特定的特性:
```cpp
std::vector<bool> features(const Widget& w);
```
進一步的,假設第五個bit表示`Widget`是否有高優先級。我們可以這樣寫代碼:
```cpp
Widget w;
…
bool highPriority = features(w)[5]; // w是不是個高優先級的?
…
processWidget(w, highPriority); // 配合優先級處理w
```
這份代碼沒有任何問題。它工作正常。但是如果我們做一個看起來無傷大雅的修改,把`highPriority`的顯式的類型換成`auto`:
```cpp
auto highPriority = features(w)[5]; // w是不是個高優先級的?
```
情況變了。所有的代碼還是可以編譯,但是他的行為變得不可預測:
```cpp
processWidget(w, highPriority); // 未定義行為
```
正如注釋中所提到的,調用`processWidget`現在會導致未定義的行為。但是為什么呢?答案是非常的令人驚訝的。在使用`auto`的代碼中,`highPriority`的類型已經不是`bool`了。盡管`std::vector<bool>`從概念上說是`bool`的容器,對`std::vector<bool>`的`operator[]`運算符并不一定是返回容器中的元素的引用(`std::vector::operator[]`對所有的類型都返回引用,就是除了`bool`)。事實上,他返回的是一個`std::vector<bool>::reference`對象(是一個在`std::vector<bool>`中內嵌的class)。
`std::vector<bool>::reference`存在是因為`std::vector<bool>`是對`bool`數據封裝的模板特化,一個bit對應一個`bool`。這就給`std::vector::operator[]`帶來了問題,因為`std::vector<T>`的`operator[]`應該返回一個`T&`,但是C++禁止bits的引用。沒辦法返回一個`bool&`,`std::vector<T>`的`operator[]`于是就返回了一個行為上和`bool&`相似的對象。想要這種行為成功,`std::vector<bool>::reference`對象必須能在`bool&`的能處的語境中使用。在`std::vector<bool>::reference`對象的特性中,是他隱式的轉換成`bool`才使得這種操作得以成功。(不是轉換成`bool&`,而是`bool`。去解釋詳細的`std::vector<bool>::reference`對象如何模擬一個`bool&`的行為有有些偏離主題,所以我們就只是簡單的提一下這種隱式轉換只是這種技術中的一部。)
在大腦中帶上這種信息,再次閱讀原先的代碼:
```cpp
bool highPriority = features(w)[5]; // 直接顯示highPriority的類型
```
這里,`features`返回了一個`std::vector<bool>`對象,在這里`operator[]`被調用。`operator[]`返回一個`std::vector<bool>::reference`對象,這個然后隱式的轉換成`highPriority`需要用來初始化的`bool`類型。于是就以`features`返回的`std::vector<bool>`的第五個bit的數值來結束`highPriority`的數值,這也是我們所預期的。
和使用`auto`的`highPriority`聲明進行對比:
```cpp
auto highPriority = features(w)[5]; // 推導highPriority的類型
```
這次,`features`返回一個`std::vector<bool>`對象,而且,`operator[]`再次被調用。`operator[]`繼續返回一個`std::vector<bool>::reference`對象,但是現在有一個變化,因為`auto`推導`highPriority`的類型。`highPriority`根本并沒有`features`返回的`std::vector<bool>`的第五個bit的數值。
數值和`std::vector<bool>::reference`是如何實現的是有關系的。一種實現是這樣的對象包含一個指向包含bit引用的機器word的指針,在word上面加上偏移。考慮這個對`highPriority`的初始化的意義,假設`std::vector<bool>::reference`的實現是恰當的。
調用`features`會返回一個臨時的`std::vector<bool>`對象。這個對象是沒有名字的,但是對于這個討論的目的,我會把它叫做`temp`,`operator[]`是在`temp`上調用的,`std::vector<bool>::reference`返回一個由`temp`管理的包含一個指向一個包含bits的數據結構的指針,在word上面加上偏移定位到第五個bit。`highPriority`也是一個`std::vector<bool>::reference`對象的一份拷貝,所以`highPriority`也在`temp`中包含一個指向word的指針,加上偏移定位到第五個bit。在這個聲明的結尾,`temp`被銷毀,因為它是個臨時對象。因此,`highPriority`包含一個野指針,這也就是調用`processWidget`會造成未定義的行為的原因:
```cpp
processWidget(w, highPriority); // 未定義的行為,highPriority包含野指針
```
`std::vector<bool>::reference`是代理類的一個例子:一個類的存在是為了模擬和對外行為和另外一個類保持一致。代理類在各種各樣的目的上被使用。`std::vector<bool>::reference`的存在是為了提供一個對`std::vector<bool>`的`operator[]`的錯覺,讓它返回一個對bit的引用,而且標準庫的智能指針類型(參考第4章)也是一些對托管的資源的代理類,使得他們的資源管理類似于原始指針。代理類的功能是良好確定的。事實上,“代理”模式是軟件設計模式中的最堅挺的成員之一。
一些代理類被設計用來隔離用戶。這就是`std::shared_ptr`和`std::unique_ptr`的情況。另外一些代理類是為了一些或多或少的不可見性。`std::vector<bool>::reference`就是這樣一個“不可見”的代理,和他類似的是`std::bitset`,對應的是`std::bitset::reference`。
同時在一些C++庫里面的類存在一種被稱作表達式模板的技術。這些庫最開始是為了提高數值運算的效率。提供一個`Matrix`類和`Matrix`對象`m1, m2, m3 and m4`,舉一個例子,下面的表達式:
```cpp
Matrix sum = m1 + m2 + m3 + m4;
```
可以計算的更快如果`Matrix`的`operator+`返回一個結果的代理而不是結果本身。這是因為,對于兩個`Matrix`,`operator+`可能返回一個類似于`Sum<Matrix, Matrix>`的代理類而不是一個`Matrix`對象。和`std::vector<bool>::reference`一樣,這里會有一個隱式的從代理類到`Matrix`的轉換,這個可能允許`sum`從由`=`右邊的表達式產生的代理對象進行初始化。(其中的對象可能會編碼整個初始化表達式,也就是,變成一種類似于`Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>`的類型。這是一個客戶端需要屏蔽的類型。)
作為一個通用的法則,“不可見”的代理類不能和`auto`愉快的玩耍。這種類常常它的生命周期不會被設計成超過一個單個的語句,所以創造這樣的類型的變量是會違反庫的設計假定。這就是`std::vector<bool>::reference`的情況,而且我們可以看到這種違背約定的做法會導致未定義的行為。
因此你要避免使用下面的代碼的形式:
```cpp
auto someVar = expression of "invisible" proxy class type;
```
但是你怎么能知道代理類被使用呢?軟件使用它們的時候并不可能會告知它們的存在。它們是不可見的,至少在概念上!一旦你發現了他們,難道你就必須放棄使用`auto`加之條款5所聲明的`auto`的各種好處嗎?
我們先看看怎么解決如何發現它們的問題。盡管“不可見”的代理類被設計用來fly beneath programmer radar in day-to-day use,庫使用它們的時候常常會撰寫關于它們的文檔來解釋為什么這樣做。你對你所使用的庫的基礎設計理念越熟悉,你就越不可能在這些庫中被代理的使用搞得狼狽不堪。
當文檔不夠用的時候,頭文件可以彌補空缺。很少有源碼封裝一個完全的代理類。它們常常從一些客戶調用者期望調用的函數返回,所有函數簽名常常可以表征它們的存在。這里是`std::vector<bool>::operator[]`的例子:
```cpp
namespace std { // from C++ Standards
template <class Allocator>
class vector<bool, Allocator> {
public:
…
class reference { … };
reference operator[](size_type n);
…
};
}
```
假設你知道對`std::vector<T>`的`operator[]`常常返回一個`T&`,在這個例子中的這種非常規的`operator[]`的返回類型一般就表征了代理類的使用。在你正在使用的這些接口之上加以關注常常可以發現代理類的存在。
在實踐上,很多的開發者只會在嘗試修復一些奇怪的編譯問題或者是調試一些錯誤的單元測試結果中發現代理類的使用。不管你是如何發現它們,一旦`auto`被決定作為推導代理類的類型而不是它被代理的類型,它就不需要涉及到關于`auto`,`auto`自己本身沒有問題。問題在于`auto`推導的類型不是所想讓它推導出來的類型。解決方案就是強制一個不同的類型推導。我把這種方法叫做顯式的類型初始化原則。
顯式的類型初始化原則涉及到使用`auto`聲明一個變量,但是轉換初始化表達式到`auto`想要的類型。下面就是一個強制`highPriority`類型是`bool`的例子:
```cpp
auto highPriority = static_cast<bool>(features(w)[5]);
```
這里,`features(w)[5]`還是返回一個`std::vector<bool>::reference`的對象,就和它經常的表現一樣,但是強制類型轉換改變了表達式的類型成為`bool`,然后`auto`才推導其作為`highPriority`的類型。在運行的時候,從`std::vector<bool>::operator[]`返回的`std::vector<bool>::reference`對象支持執行轉換到`bool`的行為,作為轉換的一部分,從`features`返回的任然存活的指向`std::vector<bool>`的指針被間接引用。這樣就在運行的開始避免了未定義行為。索引5然后放置在bits指針的偏移上,然后暴露的`bool`就作為`highPriority`的初始化數值。
針對于`Matrix`的例子,顯示的類型初始化原則可能會看起來是這樣的:
```cpp
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);
```
關于這個原則下面的程序并不禁止初始化但是要排除代理類類型。強調你要謹慎地創建一個類型的變量,它和從初始化表達式生成的類型是不同的也是有幫助意義的。舉一個例子,假設你有一個函數去計算一些方差:
```cpp
double calcEpsilon(); // 返回方差
```
`calcEpsilon`明確的返回一個`double`,但是假設你知道你的程序,`float`的精度就夠了的時候,而且你要關注`double`和`float`的長度的區別。你可以聲明一個`float`變量去存儲`calcEpsilon`的結果:
```cpp
float ep = calcEpsilon(); // 隱式轉換double到float
```
但是這個會很難表明“我故意減小函數返回值的精度”,一個使用顯式的類型初始化原則是這樣做的:
```cpp
auto ep = static_cast<float>(calcEpsilon());
```
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款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