Effective Modern C++ 條款9 用別名聲明(alias declaration)代替typedef

用別名聲明(alias declaration)代替typedef

我相信你像我一樣覺得使用STL容器是個好主意,我希望在條款18中能讓你相信使用std::unique_ptr也是個好主意,但我猜我們都不喜歡寫超過一次這樣的類型的代碼:std::unique_ptr<std::unordered_map<std::string,std::string>>。一想到這麼多的代碼就覺得增加了得腕管綜合徵的風險。

避免得病比較簡單。引進typedef

typedef 
  std::unique_ptr<std::unordered_map<std::string, std::string>>
  UPtrMapSS;

但是typedef太太太C++98了,它雖然可以在C++11中正常工作,但是C++11提供了別名聲明(alias declaration):

using UPtrMapSS = 
  std::unique_ptr<std::unordered_map<std::string, std::string>>;

就算typedef和別名聲明的作用是一樣,我們也有可靠的技術上的原因建議你使用別名聲明。

在我們講關於技術上的原因之前,我想說很多人覺得在處理函數指針時,使用別名聲明比使用typedef簡單:

// PF是一個參數爲int和const std::string&,返回值爲void的函數指針的同義詞

typedef void (*PF)(int, const std::string &);

using PF = void (*)(int, const std::string &);

當然,這兩種形式都是特別容易地被我們啃下,一些人還是需要時間來處理函數指針類型的同義詞,所以說,這並不是強迫我們使用別名聲明代替typedef的原因。

但強迫我們的原因還是存在的:模板。特別是別名聲明可以實例化(這種情況叫做alias template),而typedef不可以。這給了C++11的開發者一種直截了當的方式來表達C++98必須把typedef嵌套在結構的事情。例如,試圖定義一個鏈表的同義詞,這個鏈表使用自定義的分配器MyAlloc。使用別名聲明,這就是小case:

template <typename T>`
using MyAllocList = std::list<T, MyAlloc<T>>;

MyAllocList<Widget> lw;    // 用戶代碼

使用typedef的話,就比較麻煩了:

template <typename T>
struct MyAllocList { 
  typedef std::list<T, MyAlloc<T>> type;
};

MyAllocList<Widget>::type lw;    // 用戶代碼

這種方式不好。如果你想要在模板中使用這個typedef來創建一個鏈表,鏈表的對象是模板參數類型,那麼你必須在typedef同義詞之前使用typename

template <typename T>
class Widget {
private:
  typename MyAllocList<T>::type list;
  ...
};

這裏的代碼,MyAllocList<T>::type指的類型依賴於模板類型的參數(T)。因此MyAllocList<T>是一個依賴類型(dependent type),而C++一個受很多人喜歡的規則是,依賴類型之前必須緊挨着typename

如果MyAllocList被定義爲alias template(別名模板),就不需要typename啦:

template <typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;  // 如前

template <typename T>
class Widget {
private:
  MyAllocList<T> list;   //沒有typename,也沒有::type
  ...
};

對於你,MyAllocList<T>(使用別名模板)和MyAllocList<T>::type(內嵌typedef)看起來一樣,只依賴模板參數T,但你不是編譯器。當編譯器處理Widget模板時遇到MyAllocList<T>,它知道MyAllocList<T>是個別名模板:它一定是個類型的名字。所以MyAllocList<T>是個非依賴類型(non-dependent type),不需要也不允許關鍵字typename修飾。

另一方面,當編譯器在Widget模板看到MyAllocList<T>::type時,它不能確定這是不是一個類型的名字,因爲可能有一個MyAllocList的特例化完全沒看到MyAllocList<T>::type是一個類型。這聽起來很瘋狂,但你不能責怪編譯器擔心這個可能性,人們習慣寫這種代碼。

例如,一些誤入歧途的靈魂可能在圖謀着一些這樣的事情:

class Wine {...};

template<>      // 模板特例化
class MyAllocList<Wine> {
private:
  enum class WineType  // 關於枚舉類,請看條款10
  { White, Red, Rose };

  WineType type;  // 在這個類中,type是一個成員變量
  ...
};

正如你所見,MyAllocList<Wine>::type指代的不是類型。如果WidgetWine實例化,那麼Widget模板內的MyAllocList<T>::type指代的是一個數據成員,而不是類型。然後在Widget模板內,MyAllocList<T>::type是否指代一個類型,要依賴於參數T。這就是爲什麼編譯器就要求你在它名字前加typename,這樣做後編譯器斷言它是個類型,不用再等到模板實例化時才確定。

以下關於模板元編程方面的知識,由網友fesdobat翻譯。

如果你做過模板元編程(templete meta programming, TMP),你肯定會遇到過要把模板類型轉換爲其他類型的需求。例如,對於類型T,你可能想要去除T含有的const 修飾或者引用修飾,具體來說,比如你可能會想把const string &變成std::string。或者你也有可能想把一個類型加上const修飾或者左值引用修飾,具體來說,比如把Widget變成const Widget或者Widget &。(如果你還沒有碰到過模板元編程,那可真是太糟糕了,因爲如果你想成爲真正高效的C++程序員,你就需要對c++的這一塊至少有最基本的瞭解。你會在條款25和條款29中也看到TMP的例子,其中也包含了我在上面提及到的類型轉換。)

C++11提供了實行這樣轉換的工具,他們叫做type traits,是一種變形的模板類,包含在頭文件<type_traits>中。這個頭文件中有一系列這樣的type traits,但並不是所有的都與處理類型轉換相關,其中的一些提供瞭望文而知意的接口。假設你要轉換的源類型爲T,你需要的轉換結果就是std::transformation<T>::type。例如:

std::remove_const<T>::type // yields T from const T

std::remove_reference<T>::type // yields T from T& and T&&

std::add_lvalue_reference<T>::type // yields T& from T

上面的註釋只是簡單說明代碼做了什麼。不要在意我的修辭手法。不過在把他們用到工程中之前,你一定去會仔細查閱他們的說明文檔的,這我知道。

不過我在這裏的原意也不是上一堂關於type traits的教學課。我請你注意到這些轉換接口的末尾都帶有“::type”的後綴。如果你用它們在模板內部作爲參數傳遞(實際上你總是這樣使用它們),你也不得不在每個前面加上一個“typename”。會出現這樣像是馬路兩邊的“路牙”的玩意,是因爲c++11的type traits的實現,是基於模板化結構的內部嵌套typedef。沒錯,那就是我曾經極力向你推薦的類型變化技巧,現在他們輸給了模板別名(alias template)!!(譯註:即using關鍵字)

c++11這樣做是有歷史原因的,不過我們先跳過他們(說明很無趣,我保證)。長話短說,因爲c++標準化委員會終於認識到模板別名是更好的實現方案,他們於是先在c++14裏爲這些c++11風格的類型轉換做了一層包裝。包裝具有如下形式:對每個c++11形如std::transformation<T>::type的轉換接口,就有一個與之對應的C++14 模板別名std::transformation_t。看例子就知道我說的意思了。


std::remove_const<T>::type // C++11: const T → T

std::remove_const_t<T> // C++14 equivalent

std::remove_reference<T>::type // C++11: T&/T&& → T

std::remove_reference_t<T> // C++14 equivalent

std::add_lvalue_reference<T>::type // C++11: T →T&

std::add_lvalue_reference_t<T> // C++14 equivalent

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;

你瞧,簡單得不能再簡單了吧。

總結

需要記住的3點:

  • typedef不支持模板化,但別名聲明(alias declaration)支持。
  • 別名聲明(alias declaration)可以避免“::type”後綴,而且在模板中,typedef通常需求typename前綴。
  • C++14 offers alias templates for all the C++11 type traits transformations.
發佈了18 篇原創文章 · 獲贊 43 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章