1. 程式人生 > >C++中typename關鍵字的使用方法和注意事項

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

目錄

起因

近日,看到這樣一行程式碼:

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. 另外關於typename的歷史,Stan Lippman寫過一篇文章,Stan Lippman何許人,也許你不知道他的名字,但看完這些你一定會發出,“哦,原來是他!”:他是 C++ Primer, Inside the C++ Object Model, Essential C++, C# Primer 等著作的作者,另外他也曾是Visual C++的架構師。
  4. StackOverflow上有一個非常深入的回答,感謝@Emer 在本文評論中提供此連結。

寫在結尾

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

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

(全文完)

相關推薦

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

目錄起因近日,看到這樣一行程式碼:typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor; 雖說已經有多年C++經驗,但上面這短短一行程式碼卻看得我頭皮發麻。看起來它

java介面(interface)及使用方法注意事項

1、介面:一種把類抽象的更徹底,接口裡只能包含抽象方法的“特殊類”。介面不關心類的內部狀態資料,定義的是一批類所遵守的規範。(它只規定這批類裡必須提供某些方法,提供這些方法就可以滿足實際要求)。 在JAVA程式語言中是一個抽象型別,是抽象方法的集合,介面通常以interface來宣告。一個類通過

C#呼叫C++dll方法注意事項

在實際C#開發專案中,存在如下兩種情況 C#呼叫第三方庫,而第三方庫是使用C++編寫的; 牽涉到專案原始碼保密,C#程式碼容易被反編譯,因此抽取核心演算法部分使用C++編寫 這時候就涉及C#託管程式碼與C++非託管程式碼互相呼叫。 本文介紹C#呼叫C++的方法以及在C#

elasticsearchclient.transport.sniff的使用方法注意事項

(1)通過TransportClient這個介面,我們可以不啟動節點就可以和es叢集進行通訊,它需要指定es叢集中其中一臺或多臺機的ip地址和埠,例子如下:Client client = new TransportClient() .addTr

c++結構體位域使用注意事項

1、一個位域必須儲存在同一個單元中,不能跨兩個單元。如一個單元所剩空間不夠存放另一位域時,應從下一單元起存放該位域。如下 第一個unsigned short 中 沒有足夠的空間儲存system_clo

Extjs整合struts2的jsonplugin的方法注意事項

      最近在做一個第三方表報監控的系統,要用的很多資料展示的應用,發現用extjs和struts2的jsonplugin的結合解決問題很棒,專案已經上線,現在寫下步驟以便查閱。 步驟1、在專案中新增struts2的庫。如下 步驟2、新增Google的jsonplug

NSBundle(獲取資源路徑方法)的相關使用方法注意事項

1、[NSBundle mainBundle],資料夾其實是Group,如左側的樹形檔案管理器 Build之後,檔案直接就複製到了根目錄下,於是讀取的方法,應該是這樣: NSString *earth = [[NSBundle mainBundle] pat

C#的靜態方法靜態變數的一些總結

方法: static 修飾符的方法為靜態方法,反之則是非靜態方法 靜態成員屬於類所有,非靜態成員屬於類的例項所有,無論類建立了多少例項,類的靜態成員在記憶體中只佔同一塊區域。(所有該類的例項都共享這個類的靜態成員) C#靜態方法屬於類所有,類例項化前即可使用,靜態方法只能訪

金錢草銅錢草怎麼養殖 金錢草的養殖方法注意事項

金錢草是一種常見的綠色觀葉植物,它也是一種中藥材,而且金錢草還能開出美麗的黃色小花,它既能美化環境,也能止血消腫功能解毒止痛,平時很多人都有養殖金錢草的打算,只是不知道它怎樣才能養好,今天小編就把它養殖方法寫出來告訴大家,並讓大家瞭解養殖錢草時要注意什麼。 金錢草怎麼養殖 金錢草的養殖

C++vector的用法及注意事項

#include<vector>; 一、vector 的初始化:可以有五種方式,舉例說明如下: (1)vector<int> a(10); //定義了10個整型元素的向量(尖括號中為元素型別名,它可以是任何合法的資料型別),但沒有給出初值,其值是不確

C++關鍵字newdelete

指標常與堆(heap)空間的分配有關。堆就是指一塊記憶體區域,它允許程式在執行時以指標的方式從其中申請一定數量的儲存單元(其他儲存空間的分配是在編譯時完成的),用於資料的處理。堆記憶體也稱為動態記憶體。 C語言的方法: 1.    #include <stdlib.

JAVA C/C++ string 的區別注意

所有的字串類都起源於C語言的字串,而C語言字串則是字元的陣列。C語言中是沒有字串的,只有字元陣列。       談一下C++的字串:C++提供兩種字串的表示:C風格的字串和標準C++引入的string型別。一般建議用string型別,但是實際情況中還是要使用老式C風格的字串。       1.C風格的字串:C

C++設計類時的注意事項與遵循原則

      首先要說的是預設建構函式,編譯器可以幫使用者定義一個預設建構函式,前提是使用者沒有定義任何建構函式,一旦使用者定義了某個建構函式,不管它是不是預設的,那麼編譯器都不會再幫使用者定義預設構造函數了,在使用者定義自己的預設建構函式時,要麼沒有引數,要麼所有的引數都有一

PDM匯出sql的方法注意事項(本人…

PDM生成sql的方法(應用oracle): 工具欄裡的Database--》Database Generation(Ctrl + G) Directory:匯出路徑 File name:匯出名(我寫的是myself.sql) 點選“確定”。 如果報錯:Generation aborted due to

python字串替換方法注意事項

方法有兩種: last_date = “1/2/3”   目標為"123" 之一:repalce date =last_date.replace('/','') 之二:re p = re.compile("/") date = p.sub('', last_date)

使用neo4j圖資料庫的import工具匯入資料 -方法注意事項

背景 最近我在嘗試儲存知識圖譜的過程中,接觸到了Neo4j圖資料庫,這裡我摘取了一段Neo4j的簡介: Neo4j是一個高效能的,NOSQL圖形資料庫,它將結構化資料儲存在網路上而不是表中。它是一個嵌入式的、基於磁碟的、具備完全的事務特性的Java持

python2,python3子類呼叫父類初始化函式的方法注意事項

python2、python3: python子類呼叫父類初始化函式有兩種方式,以下程式碼在python2和python3都能執行: class A(object): def __init__(self, x): self.x = x # 方法

使用 MPMoviePlayerController 出現的問題、解決方法注意事項

 在SDK3.2及SDK4.x中MPMoviePlayerController有下面這些改動,像實現豎屏播放不再需要使用私有API了。 - In 3.1 and earlier versions, MPMoviePlayerController was full-scre

C++巨集定義的使用注意事項

1 簡介 巨集定義是C語言的三種預處理功能之一,另外兩種預處理是檔案包含和條件編譯。 1.1 格式 巨集定義的格式分為不帶引數和帶引數兩種。 不帶引數的格式為 #define 巨集名 字串 帶引數的格式為 #define 巨集名(引數表) 字串 1.2 使用 巨集展開是在預

mysql資料庫從window遷移的linux的方法注意事項

一般情況下Mysql從window遷移到linux的時候,網上都會有標準的教程如下: 1) 在windows平臺上進入/mysql/bin目錄(假設你的資料庫名字是mydata)       執行mysqldump 命令將你的資料庫匯出,具體命令如下: