1. class和typename意義相同的例子 問題:在下麵的模板聲明中class和typename的區別是什麼? 答案:沒有任何區別。當聲明一個模板類型參數時,class和typename意味著相同的事情。一些程式員喜歡使用class,因為容易敲打。其他的(包括我)更加喜歡使用typename, ...
1. class和typename含義相同的例子
問題:在下麵的模板聲明中class和typename的區別是什麼?
1 template<class T> class Widget; // uses “class” 2 3 template<typename T> class Widget; // uses “typename”
答案:沒有任何區別。當聲明一個模板類型參數時,class和typename意味著相同的事情。一些程式員喜歡使用class,因為容易敲打。其他的(包括我)更加喜歡使用typename,因為用它表明參數不需要是一個class類型。一些程式員在允許使用任何type的時候使用typename,只用對用戶自定義的類型使用class。但是從C++ 的觀點來看,在聲明模板參數的時候class和typename意味著相同的事情。
2. 必須使用typename的例子
然而,C++並不總是將class和typename同等對待。有時你必須使用typename。為了理解在什麼時候必須使用,我們必須討論能夠在模板中引用的兩種名字。
假設我們有一個函數模板,用和STL相容的容器作為模板參數,此容器中包含的對象能夠被賦值給int類型。進一步假設這個函數列印容器中的第二個元素值。我在下麵以愚蠢的方式實現了一個愚蠢的函數,它甚至不能通過編譯,但是請忽略這些事情,看下麵的例子:
1 template<typename C> // print 2nd element in 2 void print2nd(const C& container) // container; 3 { // this is not valid C++! 4 if (container.size() >= 2) { 5 C::const_iterator iter(container.begin()); // get iterator to 1st element 6 ++iter; // move iter to 2nd element 7 int value = *iter; // copy that element to an int 8 9 std::cout << value; // print the int 10 11 } 12 13 }
我對此函數中的兩個本地變數做了高亮,iter和value。Iter的類型是C::const_iterator,它依賴於模板參數C。模板中依賴於模板參數的名字被稱作依賴名字(dependent names)。當一個依賴名字嵌套在一個類中的時候,我把它叫做內嵌依賴名字(nested dependent name)。C::const_iterator是一個內嵌依賴名字。事實上,它是一個內嵌依賴類型名字(nested dependent type name),也即是指向一個類型(type)的內嵌依賴名字。
對於print2nd中的其他本地變數,value,類型為int。int不依賴於任何模板參數。這種名字被稱作“非依賴名字”(non-dependent names)。(我不知道為什麼不把它們叫做獨立名字(independent names)。“non-dependent”是一種不好的命名方式,但畢竟它是術語,所以需要遵守這個約定。)
內嵌依賴名字會導致解析困難。例如,如果我們讓print2nd函數以下麵的方式開始,會更加愚蠢:
1 template<typename C> 2 3 void print2nd(const C& container) 4 5 { 6 7 C::const_iterator * x; 8 9 ... 10 11 }
看上去像是我們聲明瞭一個本地變數x,這個x指針指向一個C::const_iterator。但是它看上去是這樣的僅僅因為我們“知道”C::const_iterator是一個type。但是如果C::const_iterator不是一個type會是怎樣呢?如果C有個靜態數據成員恰好被命名為const_iterator會發生什麼?如果x恰巧是一個全局變數的名字呢?在這種情況下,上面的code就不會聲明一個本地變數,它會是C::const_iterator和x的乘積!聽起來有些瘋狂,但這是可能的,實現C++編譯器的人員也必須考慮到所有可能的輸入,包括一些看起來很瘋狂的例子。
直到C被確定之前,沒有辦法知道C::const_iterator是否是一個type,當函數模板print2nd被解析的時候,C不能夠被確認。為了處理這種模棱兩可的問題,C++有一個準則:如果解析器在模板中碰到了一個內嵌依賴名字,它不會認為這是一個type,除非你告訴它。預設情況下,內嵌依賴名字不是types。(對於這個規則有個例外,一會會提到。)
將上面的規則記在心中,再看一次print2nd的開始部分:
1 template<typename C> 2 void print2nd(const C& container) 3 { 4 if (container.size() >= 2) { 5 C::const_iterator iter(container.begin()); // this name is assumed to 6 ... // not be a type
現在應該清楚為什麼這不是有效的C++了。Iter的聲明只有在C::const_iterator是一個type的情況下才有意義,但是我們並沒有告知C++它是一個類型,於是C++假設它不是一個類型。為了糾正這種情況,我們必須告訴C++ C::const_iterator是一個類型。我們將typename放在type之前就能達到這個目的:
1 template<typename C> // this is valid C++ 2 3 void print2nd(const C& container) 4 5 { 6 7 if (container.size() >= 2) { 8 9 typename C::const_iterator iter(container.begin()); 10 11 ... 12 13 } 14 15 }
這個規則很簡單:在一個模板中,任何時候你引用一個內嵌依賴類型名字,你都必須在名字前加上typename。(也有例外,一會會提到。)
typename應該只被用來確認一個內嵌依賴類型名字;其他的名字不應該加這個首碼。例如,下麵的函數模板使用兩個參數,一個容器和一個容器的迭代器:
1 template<typename C> // typename allowed (as is “class”) 2 void f(const C& container, // typename not allowed 3 typename C::iterator iter); // typename required
C不是內嵌依賴類型名字(它沒有內嵌在任何依賴於模板參數的東西中),所以在聲明容器的時候不應該加typename,但是C::iterator是一個內嵌依賴類型名字,所以需要加typename。
3. 一個例外——不能使用typename的地方
”typename”必須加在內嵌依賴類型名字之前“這個規則有一個例外:基類列表中的內嵌依賴類型名字或者成員初始化列表中的基類標識符不能加typename。例如:
1 template<typename T> 2 class Derived: public Base<T>::Nested { // base class list: typename not 3 4 public: // allowed 5 6 explicit Derived(int x) 7 8 9 10 : Base<T>::Nested(x) // base class identifier in mem. 11 12 { // init. list: typename not allowed 13 14 15 typename Base<T>::Nested temp; // use of nested dependent type 16 ... // name not in a base class list or 17 } // as a base class identifier in a 18 ... // mem. init. list: typename 19 required 20 };
這種不一致性令人感到厭煩,但是一旦你有了一點經驗,你就會註意到它。
4. 最後的例子——為typename使用typedef
讓我們看最後一個typename的例子,因為它代表了你將會在真實代碼中看到的某些東西。假設我們正在實現一個函數模板,帶了一個迭代器參數,我們想為迭代器指向的對象做一份本地拷貝,temp。我們可以像下麵這樣實現:
1 template<typename IterT> 2 void workWithIterator(IterT iter) 3 { 4 typename std::iterator_traits<IterT>::value_type temp(*iter); 5 ... 6 }
不要讓 std::iterator_traits<IterT>::value_type 嚇到你。這隻是標準特性類(standard traits class)的一種使用方法,這是“類型IterT對象指向的類型“的C++實現方式。這個句子聲明瞭一個本地變數(temp),它的類型同IterT對象指向的對象的類型一致,它將temp初始化為iter指向的對象。如果IterT是vector<int>::iterator,那麼temp就是int類型的。如果IterT是list<string>::iterator,temp就是string類型的。因為std::iterator_traits<IterT>::value_type是一個內嵌依賴類型名字(在iterator_traits<IterT>內部value_type是內嵌的,IterT是一個模板參數),我們必須為其添加typename。
如果你認為讀std::iterator_traits<IterT>::value_type是一件不讓人愉快的事情,想像一下將其打出來會是什麼樣的。如果你像大部分程式員一樣,多次輸入這個表達式的想法是可怕的,所以你會想為其創建一個typedef。對於像value_type這樣的特性(traits)成員名字來說(對於特性的信息看Item47),使用慣例是使得typedef名字和特性成員名字相同,所以這樣一個本地typedef通常被定義成下麵這樣:
1 template<typename IterT> 2 void workWithIterator(IterT iter) 3 { 4 typedef typename std::iterator_traits<IterT>::value_type value_type; 5 value_type temp(*iter); 6 ... 7 }
許多程式員發現將“typedef typename“併列看上去不和諧,但是對於使用內嵌依賴類型名字的規則來說,這是一個合乎邏輯的結果。你會很快習慣這種用法。畢竟,你有著很強的驅動力。你想輸入typename std::iterator_traits<IterT>::value_type多少次呢?
5. Typename的執行因編譯器而異
作為結束語,我應該提及的是關於typename規則的強制執行隨著編譯器的不同而不同,一些編譯器接受需要typename但實際上沒有輸入的情況;一些編譯器接受輸入了typename但實際上不允許的情況;還有一些(通常是老的編譯器)在需要輸入typename時拒絕了typename輸入。這就意味著typename和內嵌依賴類型名字的交互會產生讓你頭痛的問題。
6. 總結
- 當聲明模板參數的時候,class和typename是可以互換的。
- 使用typename來識別內嵌依賴類型名字,但在基類列表中或者成員初始化列表中的基類標識符除外。