C++ typename的起源與用法
侯捷在Effective C++的中文版譯序中提到:
C++的難學,還在于它提供了四種不同(但相輔相成)的程式設計思維模式:procedural-based, object-based, object-oriented, generics
對于較少使用最後一種泛型程式設計的我來說,程式設計基本上停留在前三種思維模式當中。雖說不得窺見高深又現代的泛型技術,但前三種思維模式已幾乎滿足我所遇到的所有需求,是以一直未曾深入去了解泛型程式設計。
目錄
- 起因
- typename的常見用法
- typename的來源
- 一些關鍵概念
- 限定名和非限定名
- 依賴名和非依賴名
- 類作用域
- 引入typename的真實原因
- 一個例子
- 問題浮現
- 千呼萬喚始出來
- 不同編譯器對錯誤情況的處理
- 使用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),故名思義,是限定了命名空間的名稱。看下面這段代碼,
cout
和
endl
就是限定名:
#include <iostream>
int main() {
std::cout << "Hello world!" << std::endl;
}
cout
和
endl
前面都有
std::
,它限定了
std
這個命名空間,是以稱其為限定名。
如果在上面這段代碼中,前面用
using std::cout;
或者
using namespace std;
,然後使用時隻用
cout
和
endl
,它們的前面不再有空間限定
std::
,是以此時的
cout
和
endl
就叫做非限定名(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
。是以,
T
,
vector<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::A
,
MyClass::B
,
MyClass::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隻能用于模闆的定義中
- 非限定類型,比如前面介紹過的
,int
之類vector<int>
- 基類清單中,比如
不能在template <class T> class C1 : T::InnerType
前面加typenameT::InnerType
- 構造函數的初始化清單中
- 如果類型是依賴于模闆參數的限定名,那麼在它之前必須加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
,會出現什麼情況。
對于指定模闆參數這一用法,雖然
class
和
typename
都支援,但就個人而言我還是傾向使用
typename
多一些,因為我始終過不了
class
表示使用者定義類型這道坎。另外,從語義上來說,
typename
比
class
表達的更為清楚。C++ Primer也建議使用
typename
:
參考
- C++ Primer
- Effective C++
- A Description of the C++ typename keyword
- 維基百科typename
- 另外關于
的曆史,Stan Lippman寫過一篇文章,Stan Lippman何許人,也許你不知道他的名字,但看完這些你一定會發出,“哦,原來是他!”:他是 C++ Primer, Inside the C++ Object Model, Essential C++, C# Primer 等著作的作者,另外他也曾是Visual C++的架構師。typename
- 在StackOverflow上有一個非常深入的回答,感謝@Emer 在本文評論中提供此連結。