條款9:優先使用聲明別名而不是typedef
我有信心說,大家都同意使用STL
容器是個好的想法,并且我希望,條款18可以說服你使用std::unique_ptr
也是個好想法,但是我想絕對我們中間沒有人喜歡寫像這樣std::unique_ptr<std::unordered_map<std::string, std::string>>
的代碼多于一次。這僅僅是考慮到這樣的代碼會增加得上“鍵盤手”的風險。
為了避免這樣的醫療悲劇,推薦使用一個typedef
:
typedefstd::unique_ptr<std::unordered_map<std::string, std::string>>UPtrMapSS;
但是typedef
家族是有如此濃厚的C++98
氣息。他們的確可以在C++11
下工作,但是C++11
也提供了聲明別名(alias declarations
):
using UptrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;
考慮到typedef
和聲明別名具有完全一樣的意義,推薦其中一個而排斥另外一個的堅實技術原因是容易令人質疑的。這樣的質疑是合理的。
技術原因當然存在,但是在我提到之前。我想說的是,很多人發現使用聲明別名可以使涉及到函數指針的類型的聲明變得容易理解:
// 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
的鏈接表定義一個標識符。使用別名模板,這就是小菜一碟:
template<typname T> // MyAllocList<T>using MyAllocList = std::list<T, MyAlloc<T>>; // 等同于// std::list<T,// MyAlloc<T>>MyAllocList<Widget> lw; // 終端代碼
使用typedef
,你不得不從草稿圖開始去做一個蛋糕:
template<typename T> // MyAllocList<T>::typestruct MyAllocList { // 等同于typedef std::list<T, MyAlloc<T>> type; // std::list<T, }; // MyAlloc<T>>MyAllocList<Widget>::type lw; // 終端代碼
如果你想在一個模板中使用typedef
來完成創建一個節點類型可以被模板參數指定的鏈接表的任務,你必須在typedef
名稱之前使用typename
:
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
后綴):
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
表示的并不是一個類型。這聽上去挺瘋狂的,但是不要因為這種可能性而怪罪于編譯器。是人類有可能會寫出這樣的代碼。
例如,一些被誤導的鬼魂可能會雜糅出像這樣代碼:
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
。例如:
std::remove_const<T>::type // 從 const T 得到 Tstd::remove_reference<T>::type // 從 T& 或 T&& 得到 Tstd::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
。用例子來說明我的意思:
std::remove_const<T>::type // C++11: const T -> Tstd::remove_const_t<T> // 等價的C++14std::remove_reference<T>::type // C++11: T&/T&& -> Tstd::remove_reference_t<T> // 等價的C++14std::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
標準的電子拷貝,這依然很簡單,因為需要做的即使一些復制和粘貼操作。在這里,我給你開個頭:
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 中的類型特征轉換提供了模板別名 |