條款9:優先使用聲明別名而不是`typedef`
=========================
我有信心說,大家都同意使用`STL`容器是個好的想法,并且我希望,條款18可以說服你使用`std::unique_ptr`也是個好想法,但是我想絕對我們中間沒有人喜歡寫像這樣`std::unique_ptr<std::unordered_map<std::string, std::string>>`的代碼多于一次。這僅僅是考慮到這樣的代碼會增加得上“鍵盤手”的風險。
為了避免這樣的醫療悲劇,推薦使用一個`typedef`:
```cpp
typedef
std::unique_ptr<std::unordered_map<std::string, std::string>>
UPtrMapSS;
```
但是`typedef`家族是有如此濃厚的`C++98`氣息。他們的確可以在`C++11`下工作,但是`C++11`也提供了聲明別名(`alias declarations`):
```cpp
using UptrMapSS =
std::unique_ptr<std::unordered_map<std::string, std::string>>;
```
考慮到`typedef`和聲明別名具有完全一樣的意義,推薦其中一個而排斥另外一個的堅實技術原因是容易令人質疑的。這樣的質疑是合理的。
技術原因當然存在,但是在我提到之前。我想說的是,很多人發現使用聲明別名可以使涉及到函數指針的類型的聲明變得容易理解:
```cpp
// FP等價于一個函數指針,這個函數的參數是一個int類型和
// std::string常量類型,沒有返回值
typedef void (*FP)(int, const std::string&); // typedef
// 同上
using FP = void (*)(int, const std::string&); // 聲明別名
```
當然,上面任何形式都不是特別讓人容易下咽,并且很少有人會花費大量的時間在一個函數指針類型的標識符上,所以這很難當做選擇聲明別名而不是`typedef`的不可抗拒的原因。
但是,一個不可抗拒的原因是真實存在的:模板。尤其是聲明別名有可能是模板化的(這種情況下,它們被稱為模板別名(`alias template`)),然而`typedef`這是只能說句“臣妾做不到”。模板別名給`C++11`程序員提供了一個明確的機制來表達在`C++98`中需要黑客式的將`typedef`嵌入在模板化的`struct`中才能完成的東西。舉個栗子,給一個使用個性化的分配器`MyAlloc`的鏈接表定義一個標識符。使用別名模板,這就是小菜一碟:
```cpp
template<typname T> // MyAllocList<T>
using MyAllocList = std::list<T, MyAlloc<T>>; // 等同于
// std::list<T,
// MyAlloc<T>>
MyAllocList<Widget> lw; // 終端代碼
```
使用`typedef`,你不得不從草稿圖開始去做一個蛋糕:
```cpp
template<typename T> // MyAllocList<T>::type
struct MyAllocList { // 等同于
typedef std::list<T, MyAlloc<T>> type; // std::list<T,
}; // MyAlloc<T>>
MyAllocList<Widget>::type lw; // 終端代碼
```
如果你想在一個模板中使用`typedef`來完成創建一個節點類型可以被模板參數指定的鏈接表的任務,你必須在`typedef`名稱之前使用`typename`:
```cpp
template<typename T> // Widget<T> 包含
class Widget{ // 一個 MyAloocList<T>
private: // 作為一個數據成員
typename MyAllocList<T>::type list;
...
};
```
此處,`MyAllocList<T>::type`表示一個依賴于模板類型參數`T`的類型,因此`MyAllocList<T>::type`是一個依賴類型(`dependent type`),`C++`中許多令人喜愛的原則中的一個就是在依賴類型的名稱之前必須冠以`typename`。
如果`MyAllocList`被定義為一個聲明別名,就不需要使用`typename`(就像笨重的`::type`后綴):
```cpp
template<typname T>
using MyAllocList = std::list<T, MyAlloc<T>>; // 和以前一樣
template<typename T>
class Widget {
private:
MyAllocList<T> list; // 沒有typename
... // 沒有::type
};
```
對你來說,`MyAllocList<T>`(使用模板別名)看上去依賴于模板參數`T`,正如`MyAllocList<T>::type`(使用內嵌的`typdef`)一樣,但是你不是編譯器。當編譯器處理`Widget`遇到`MyAllocList<T>`(使用模板別名),編譯器知道`MyAllocList<T>`是一個類型名稱,因為`MyAllocList`是一個模板別名:它必須是一個類型。`MyAllocList<T>`因此是一個非依賴類型(`non-dependent type`),指定符`typename`是不需要和不允許的。
另一方面,當編譯器在`Widget`模板中遇到`MyAllocList<T>`(使用內嵌的`typename`)時,編譯器并不知道它是一個類型名,因為有可能存在一個特殊化的`MyAllocList`,只是編譯器還沒有掃描到,在這個特殊化的`MyAllocList`中`MyAllocList<T>::type`表示的并不是一個類型。這聽上去挺瘋狂的,但是不要因為這種可能性而怪罪于編譯器。是人類有可能會寫出這樣的代碼。
例如,一些被誤導的鬼魂可能會雜糅出像這樣代碼:
```cpp
class Wine {...};
template<> // 當T時Wine時
class MyAllocList<Wine>{ // MyAllocList 是特殊化的
private:
enum class WineType // 關于枚舉類參考條款10
{ White, Red, Rose };
WineType type; // 在這個類中,type是個數據成員
...
};
```
正如你看到的,`MyAllocList<Wine>::type`并不是指一個類型。如果`Widget`被使用`Wine`初始化,`Widget`模板中的`MyAllocList<T>::type`指的是一個數據成員,而不是一個類型。在`Wedget`模板中,`MyAllocList<T>::type`是否指的是一個類型忠實地依賴于傳入的`T`是什么,這也是編譯器堅持要求你在類型前面冠以`typename`的原因。
如果你曾經做過模板元編程(`TMP`),你會強烈地額反對使用模板類型參數并在此基礎上修改為其他類型的必要性。例如,給定一個類型`T`,你有可能想剝奪`T`所包含的所有的`const`或引用的修飾符,即你想將`const std::string&`變成`std::string`。你也有可能想給一個類型加上`const`或者將它變成一個左值引用,也就是將`Widget`變成`const Widget`或者`Widget&`。(如果你沒有做過`TMP`,這太糟糕了,因為如果你想成為一個真正牛叉的`C++`程序員,你至少需要對`C++`這方面的基本概念足夠熟悉。你可以同時看一些TMP的例子,包括我上面提到的類型轉換,還有條款23和條款27。)
`C++11`給你提供了工具來完成這類轉換的工作,表現的形式是`type traits`,它是`<type_traits>`中的一個模板的分類工具。在這個頭文件中有數十個類型特征,但是并不是都可以提供類型轉換,不提供轉換的也提供了意料之中的接口。給定一個你想競選類型轉換的類型`T`,得到的類型是`std::transformation<T>::type`。例如:
```cpp
std::remove_const<T>::type // 從 const T 得到 T
std::remove_reference<T>::type // 從 T& 或 T&& 得到 T
std::add_lvalue_reference<T>::type // 從 T 得到 T&
```
注釋僅僅總結了這些轉換干了什么,因此不需要太咬文嚼字。在一個項目中使用它們之前,我知道你會參考準確的技術規范。
無論如何,我在這里不是只想給你大致介紹一下類型特征。反而是因為注意到,類型轉換總是以`::type`作為每次使用的結尾。當你對一個模板中的類型參數(你在實際代碼中會經常用到)使用它們時,你必須在每次使用前冠以`typename`。這其中的原因是`C++11`的類型特征是通過內嵌`typedef`到一個模板化的`struct`來實現的。就是這樣的,他們就是通過使用類型同義技術來實現的,就是我一直在說服你遠不如模板別名的那個技術。
這是一個歷史遺留問題,但是我們略過不表(我打賭,這個原因真的很枯燥)。因為標準委員會姍姍來遲地意識到模板別名是一個更好的方式,對于`C++11`的類型轉換,委員會使這些模板也成為`C++14`的一部分。別名有一個統一的形式:對于`C++11`中的每個類型轉換`std::transformation<T>::type`,有一個對應的`C++14`的模板別名`std::transformation_t`。用例子來說明我的意思:
```cpp
std::remove_const<T>::type // C++11: const T -> T
std::remove_const_t<T> // 等價的C++14
std::remove_reference<T>::type // C++11: T&/T&& -> T
std::remove_reference_t<T> // 等價的C++14
std::add_lvalue_reference<T>::type // C++11: T -> T&
std::add_lvalue_reference_t<T> // 等價的C++14
```
`C++11`的結構在`C++14`中依然有效,但是我不知道你還有什么理由再用他們。即便你不熟悉`C++14`,自己寫一個模板別名也是小兒科。僅僅`C++11`的語言特性被要求,孩子們甚至都可以模擬一個模式,對嗎?如果你碰巧有一份`C++14`標準的電子拷貝,這依然很簡單,因為需要做的即使一些復制和粘貼操作。在這里,我給你開個頭:
```cpp
template<class T>
using remove_const_t = typename remove_const<T>::type;
template<class T>
using remove_reference_t = typename remove_reference<T>::type;
template<class T>
using add_lvalue_reference_t =
typename add_lvalue_reference<T>::type;
```
看到沒有?不能再簡單了。
|要記住的東西|
|:--------- |
|`typedef`不支持模板化,但是別名聲明支持|
|模板別名避免了`::type`后綴,在模板中,`typedef`還經常要求使用`typename`前綴|
|`C++14`為`C++11`中的類型特征轉換提供了模板別名|
- 出版者的忠告
- 致謝
- 簡介
- 第一章 類型推導
- 條款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