轉載:C++ typename的起源與用法

轉載:http://feihu.me/blog/2014/the-origin-and-usage-of-typename/#typenamehtml

侯捷在Effective C++的中文版譯序中提到:ios

C++的難學,還在於它提供了四種不一樣(但相輔相成)的程序設計思惟模式:procedural-based, object-based, object-oriented, generics編程

對於較少使用最後一種泛型編程的我來講,程序設計基本上停留在前三種思惟模式當中。雖然說不得窺見高深又現代的泛型技術,但前三種思惟模式已幾乎知足我所遇到的全部需求,所以一直不曾深刻去了解泛型編程。瀏覽器

目錄


原由

近日,看到這樣一行代碼:markdown

typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;

雖然說已經有多年C++經驗,但上面這短短一行代碼卻看得我頭皮發麻。看起來它應該是定義一個類型別名,可是typedef不該該是像這樣使用麼,typedef+原類型名+新類型名:架構

typedef char* PCHAR;

可爲什麼此處多了一個typename?另外__type_traits又是什麼?看起來有些眼熟,想起以前在Effective C++上曾經看過traits這一技術的介紹,和這裏的__type_traits有點像。只是一直不曾遇到須要traits的時候,因此當時並未仔細研究。然而STL中大量的充斥着各類各樣的traits,一查才發現原來它是一種很是高級的技術,在更現的高級語言中已經很廣泛。所以此次花了些時間去學習它,接下來還有會有另外一篇文章來詳細介紹C++的traits技術。在這裏,咱們暫時忘記它,僅將它當成一個普通的類,先來探討一下這個多出來的typename是怎麼回事?app

typename的常見用法

對於typename這個關鍵字,若是你熟悉C++的模板,必定會知道它有這樣一種最多見的用法(代碼摘自C++ Primer):less

// implement strcmp-like generic compare function // returns 0 if the values are equal, 1 if v1 is larger, -1 if v1 is smaller template <typename T> int compare(const T &v1, const T &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }

也許你會想到上面這段代碼中的typename換成class也同樣能夠,不錯!那麼這裏便有了疑問,這兩種方式有區別麼?查看C++ Primer以後,發現二者徹底同樣。那麼爲何C++要同時支持這兩種方式呢?既然class很早就已經有了,爲何還要引入typename這一關鍵字呢?問的好,這裏面有一段不爲人知的歷史(也許只是我不知道:-))。帶着這些疑問,咱們開始探尋之旅。ide

typename的來源

對於一些更早接觸C++的朋友,你可能知道,在C++標準還未統一時,不少舊的編譯器只支持class,由於那時C++並無typename關鍵字。記得我在學習C++時就曾在某本C++書籍上看過相似的注意事項,告訴咱們若是使用typename時編譯器報錯的話,那麼換成class便可。函數

一切歸結於歷史。

Stroustrup在最初起草模板規範時,他曾考慮到爲模板的類型參數引入一個新的關鍵字,可是這樣作極可能會破壞已經寫好的不少程序(由於class已經使用了很長一段時間)。可是更重要的緣由是,在當時看來,class已徹底足夠勝任模板的這一需求,所以,爲了不引發沒必要要的麻煩,他選擇了妥協,重用已有的class關鍵字。因此只到ISO C++標準出來以前,想要指定模板的類型參數只有一種方法,那即是使用class。這也解釋了爲何不少舊的編譯器只支持class

可是對不少人來講,老是不習慣class,由於從其原本存在的目的來講,是爲了區別於語言的內置類型,用於聲明一個用戶自定義類型。那麼對於下面這個模板函數的定義(相對於上例,僅將typename換成了class):

template <class T> int compare(const T &v1, const T &v2) { if (v1 < v2) return -1; if (v2 < v1) return 1; return 0; }

從表面上看起來就好像這個模板的參數應該只支持用戶自定義類型,因此使用語言內置類型或者指針來調用該模板函數時總會以爲有一絲奇怪(雖然並無錯誤):

int v1 = 1, v2 = 2; int ret = compare(v1, v2); int *pv1 = NULL, *pv2 = NULL; ret = compare(pv1, pv2);

使人感到奇怪的緣由是,class在類和模板中表現的意義看起來存在一些不一致,前者針對用戶自定義類型,然後者包含了語言內置類型和指針。也正由於如此,人們彷佛以爲當時沒有引入一個新的關鍵字多是一個錯誤。

這是促使標準委員會引入新關鍵字的一個因素,但其實還有另一個更加劇要的緣由,和文章最開始那行代碼相關。

一些關鍵概念

在咱們揭開真實緣由的面紗以前,先保持一點神祕感,由於爲了更好的理解C++標準,有幾個重要的概念須要先行介紹一下。

限定名和非限定名

限定名(qualified name),故名思義,是限定了命名空間的名稱。看下面這段代碼,coutendl就是限定名:

#include <iostream> int main() { std::cout << "Hello world!" << std::endl; }

coutendl前面都有std::,它限定了std這個命名空間,所以稱其爲限定名。

若是在上面這段代碼中,前面用using std::cout;或者using namespace std;,而後使用時只用coutendl,它們的前面再也不有空間限定std::,因此此時的coutendl就叫作非限定名(unqualified name)。

依賴名和非依賴名

依賴名(dependent name)是指依賴於模板參數的名稱,而非依賴名(non-dependent name)則相反,指不依賴於模板參數的名稱。看下面這段代碼:

template <class T> class MyClass { int i; vector<int> vi; vector<int>::iterator vitr; T t; vector<T> vt; vector<T>::iterator viter; };

由於是內置類型,因此類中前三個定義的類型在聲明這個模板類時就已知。然而對於接下來的三行定義,只有在模板實例化時才能知道它們的類型,由於它們都依賴於模板參數T。所以,Tvector<T>vector<T>::iterator稱爲依賴名。前三個定義叫作非依賴名。

更爲複雜一點,若是用了typedef T U; U u;,雖然T沒再出現,可是U仍然是依賴名。因而可知,無論是直接仍是間接,只要依賴於模板參數,該名稱就是依賴名。

類做用域

在類外部訪問類中的名稱時,可使用類做用域操做符,形如MyClass::name的調用一般存在三種:靜態數據成員、靜態成員函數和嵌套類型:

struct MyClass { static int A; static int B(); typedef int C; }

MyClass::AMyClass::BMyClass::C分別對應着上面三種。

引入typename的真實緣由

結束以上三個概念的討論,讓咱們接着揭開typename的神祕面紗。

一個例子

在Stroustrup起草了最初的模板規範以後,人們更加無憂無慮的使用了class很長一段時間。但是,隨着標準化C++工做的到來,人們發現了模板這樣一種定義:

template <class T> void foo() { T::iterator * iter; // ... }

這段代碼的目的是什麼?多數人第一反應多是:做者想定義一個指針iter,它指向的類型是包含在類做用域T中的iterator。可能存在這樣一個包含iterator類型的結構:

struct ContainsAType { struct iterator { /*...*/ }; // ... };

而後像這樣實例化foo

foo<ContainsAType>();

這樣一來,iter那行代碼就很明顯了,它是一個ContainsAType::iterator類型的指針。到目前爲止,我們猜想的一點不錯,一切都看起來很美好。

問題浮現

在類做用域一節中,咱們介紹了三種名稱,因爲MyClass已是一個完整的定義,所以編譯期它的類型就能夠肯定下來,也就是說MyClass::A這些名稱對於編譯器來講也是已知的。

但是,若是是像T::iterator這樣呢?T是模板中的類型參數,它只有等到模板實例化時纔會知道是哪一種類型,更不用說內部的iterator。經過前面類做用域一節的介紹,咱們能夠知道,T::iterator實際上能夠是如下三種中的任何一種類型:

  • 靜態數據成員
  • 靜態成員函數
  • 嵌套類型

前面例子中的ContainsAType::iterator是嵌套類型,徹底沒有問題。可若是是靜態數據成員呢?若是實例化foo模板函數的類型是像這樣的:

struct ContainsAnotherType { static int iterator; // ... };

而後如此實例化foo的類型參數:

foo<ContainsAnotherType>();

那麼,T::iterator * iter;被編譯器實例化爲ContainsAnotherType::iterator * iter;,這是什麼?前面是一個靜態成員變量而不是類型,那麼這便成了一個乘法表達式,只不過iter在這裏沒有定義,編譯器會報錯:

error C2065: ‘iter’ : undeclared identifier

但若是iter是一個全局變量,那麼這行代碼將徹底正確,它是表示計算兩數相乘的表達式,返回值被拋棄。

同一行代碼能以兩種徹底不一樣的方式解釋,並且在模板實例化以前,徹底沒有辦法來區分它們,這絕對是滋生各類bug的溫牀。這時C++標準委員會再也忍不住了,與其到實例化時才能知道到底選擇哪一種方式來解釋以上代碼,委員會決定引入一個新的關鍵字,這就是typename

千呼萬喚始出來

咱們來看看C++標準

A name used in a template declaration or definition and that is dependent on a template-parameter is assumed not to name a type unless the applicable name lookup finds a type name or the name is qualified by the keyword typename.

對於用於模板定義的依賴於模板參數的名稱,只有在實例化的參數中存在這個類型名,或者這個名稱前使用了typename關鍵字來修飾,編譯器纔會將該名稱當成是類型。除了以上這兩種狀況,毫不會被當成是類型。

所以,若是你想直接告訴編譯器T::iterator是類型而不是變量,只需用typename修飾:

template <class T> void foo() { typename T::iterator * iter; // ... }

這樣編譯器就能夠肯定T::iterator是一個類型,而再也不須要等到實例化時期才能肯定,所以消除了前面提到的歧義。

不一樣編譯器對錯誤狀況的處理

可是若是仍然用ContainsAnotherType來實例化foo,前者只有一個叫iterator的靜態成員變量,然後者須要的是一個類型,結果會怎樣?我在Visual C++ 2010和g++ 4.3.4上分別作了實驗,結果以下:

Visual C++ 2010仍然報告了和前面同樣的錯誤:

error C2065: ‘iter’ : undeclared identifier

雖然咱們已經用關鍵字typename告訴了編譯器iterator應該是一個類型,可是用一個定義了iterator變量的結構來實例化模板時,編譯器卻選擇忽略了此關鍵字。出現錯誤只是因爲iter沒有定義。

再來看看g++如何處理這種狀況,它的錯誤信息以下:

In function ‘void foo() [with T = ContainsAnotherType]’: instantiated from here error: no type named ‘iterator’ in ‘struct ContainsAnotherType’

g++在ContainsAnotherType中沒有找到iterator類型,因此直接報錯。它並無嘗試以另一種方式來解釋,因而可知,在這點上,g++更加嚴格,更遵循C++標準。

使用typename的規則

最後這個規則看起來有些複雜,能夠參考MSDN

  • typename在下面狀況下禁止使用:
    • 模板定義以外,即typename只能用於模板的定義中
    • 非限定類型,好比前面介紹過的intvector<int>之類
    • 基類列表中,好比template <class T> class C1 : T::InnerType不能在T::InnerType前面加typename
    • 構造函數的初始化列表中
  • 若是類型是依賴於模板參數的限定名,那麼在它以前必須加typename(除非是基類列表,或者在類的初始化成員列表中)
  • 其它狀況下typename是可選的,也就是說對於一個不是依賴名的限定名,該名稱是可選的,例如vector<int> vi;

其它例子

對於不會引發歧義的狀況,仍然須要在前面加typename,好比:

template <class T> void foo() { typename T::iterator iter; // ... }

不像前面的T::iterator * iter可能會被當成乘法表達式,這裏不會引發歧義,但仍需加typename修飾。

再看下面這種:

template <class T> void foo() { typedef typename T::iterator iterator_type; // ... }

是否和文章剛開始的那行使人頭皮發麻的代碼有些許類似?沒錯!如今終於能夠解開typename之迷了,看到這裏,我相信你也必定能夠解釋那行代碼了,咱們再看一眼:

typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;

它是將__type_traits<T>這個模板類中的has_trivial_destructor嵌套類型定義一個叫作trivial_destructor的別名,清晰明瞭。

再看常見用法

既然typename關鍵字已經存在,並且它也能夠用於最多見的指定模板參數,那麼爲何不廢除class這一用法呢?答案其實也很明顯,由於在最終的標準出來以前,全部已存在的書、文章、教學、代碼中都是使用的是class,能夠想像,若是標準再也不支持class,會出現什麼狀況。

對於指定模板參數這一用法,雖然classtypename都支持,但就我的而言我仍是傾向使用typename多一些,由於我始終過不了class表示用戶定義類型這道坎。另外,從語義上來講,typenameclass表達的更爲清楚。C++ Primer也建議使用typename:

使用關鍵字typename代替關鍵字class指定模板類型形參也許更爲直觀,畢竟,可使用內置類型(非類類型)做爲實際的類型形參,並且,typename更清楚地指明後面的名字是一個類型名。可是,關鍵字typename是做爲標準C++的組成部分加入到C++中的,所以舊的程序更有可能只用關鍵字class。

參考

  1. C++ Primer
  2. Effective C++
  3. A Description of the C++ typename keyword
  4. 維基百科typename
  5. 另外關於typename的歷史,Stan Lippman寫過一篇文章,Stan Lippman何許人,也許你不知道他的名字,但看完這些你必定會發出,「哦,原來是他!」:他是 C++ Primer, Inside the C++ Object Model, Essential C++, C# Primer 等著做的做者,另外他也曾是Visual C++的架構師。
  6. StackOverflow上有一個很是深刻的回答,感謝@Emer 在本文評論中提供此連接。

寫在結尾

一個簡單的關鍵字就已經充滿曲折,這能夠從一個角度反映出一門語言的發展歷程,究竟要經歷多少決斷、波折與妥協,最終才發展成爲如今的模樣。在一個特定的時期,因爲歷史、技術、思想等各方面的因素,設計總會向現實作出必定的讓步,出現一些「不完美」的設計,爲了保持向後兼容,有些「不完美」的歷史因素被保留了下來。如今我能夠理解常常爲人所詬病的Windows操做系統,Intel芯片,IE瀏覽器,Visual C++等,爲了保持向後兼容,不得不在新的設計中仍然保留這些「不完美」,雖然帶來的是更多的優秀特性,但有些人卻總由於這些歷史因素而唾棄它們,也爲本身曾有同樣的舉動而羞愧不已。但也正是這些「不完美」的出現,才讓人們在後續的設計中更加註意,站在前人的肩膀上,作出更好,更完善的設計,因而科技纔不斷向前推動。

然而也有一些勇於大膽嘗試的例子,好比C++ 11,它的變化之大甚至連Stroustrup都說它像一門新語言。對於有着30餘年歷史的「老」語言,不只沒有被各類新貴擊潰,反而在不斷向晚輩們借鑑,吸納一些好的特性,老而彌堅,這十分不易。還有Python 3,爲了清理2.x版本中某些語法方面的問題,打破了與2.x版本的向後兼容性,這種犧牲向後兼容換取進步的作法當然延緩了新版本的接受時間,但我相信這是向前進步的陣痛。Guido van Rossum的這種破舊立新的魄力實在讓人欽佩,至於這種作法可否最終爲人們所接受,一切交給歷史來檢驗。

(全文完)

相關文章
相關標籤/搜索