C++中typename關鍵字的使用方法和注意事項

ref:https://blog.csdn.net/vanturman/article/details/80269081

起因

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

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是怎麼回事?

typename的常見用法

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

// 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這一關鍵字呢?問的好,這裏面有一段鮮爲人知的歷史(也許只是我不知道:-))。帶着這些疑問,我們開始探尋之旅。

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 hereerror: 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的這種破舊立新的魄力實在讓人欽佩,至於這種做法能否最終爲人們所接受,一切交給歷史來檢驗。

(全文完)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章