上一篇C++ template —— 模板基礎(一)講解了有關C++模板的大多數概念,日常C++程序設計中所遇到的很多問題,都可以從這部分教程得到解答。本篇中我們深入語言特性。
------------------------------------------------------------------------------------------------------------
第8章 深入模板基礎
8.1 參數化聲明
函數模板和類模板:
// 類模板 template <typename T> class List // (1)作為名字空間作用域的類模板 { public: template <typename T2> List(List<T2> const&); // (2)成員函數模板(構造函數) }; template <typename T> template <typename T2> List<T>::List(List<T2> const& b){} // (3)位於類外部的成員函數模板定義
// (4)位於外部名字空間作用域的函數模板 template <typename T> int length(List<T> const&); // 聯合(Union)模板,往往被看作類模板的一種 template <typename T> union AllocChunk { T object; usigned char bytes[sizeof(T)]; }; // 和普通函數聲明一樣,函數模板聲明也可以具有缺省調用實參(但不能具有缺省模板實參,注意兩者的不同) void fill(Array<T>*, T const& = T()); // 對於基本類型T()為0,這同時也說明了缺省調用實參可以依賴於模板參數
顯然,當fill()函數被調用時,如果提供了第2個函數調用參數的話,就不會實例化這個缺省實參。這同時說明了:即使不能基於特定類型T來實例化缺省調用實參,也可能不會出現錯誤。例如:
class Value { public: Value(int); // 提供了構造函數,不會再隱式提供缺省無參構造函數,所以,也就不存在缺省構造函數 // 如果需要無參構造函數,需要顯示聲明 }; void init(Array<Value>* array) { Value zero(0); fill(array, zero); // 正確,提供了第2個調用實參,不會使用 =T() fill(array); // 錯誤:使用了=T(),但當T=Value時缺省構造函數無效(因為顯示定義了[只有一個參數的構造函數],所以不會產生缺省的無參構造函數) }
除了兩種基本類型的模板之外,還可以使用相似的符號來參數化其他的3種聲明。這3種聲明分別都有與之對應的類模板成員的定義:
- (1)類模板的成員函數的定義;
- (2)類模板的嵌套類成員的定義;
- (3)類模板的靜態數據成員的定義;
如下:
template <int I> // int I 非類型的模板參數,I接收的不是一個類型,而是一個值(非類型) class CupBoard { void open(); class Shelf; static double total_weight; ... }; template <int I> void CupBoard<I>::open(){ ... } template <int I> class CupBoard<I>::Shelf { ... }; template <int I> double CupBoard<I>::total_weight = 0.0;
8.1.1 虛成員函數
- 成員函數模板不能被聲明為虛函數。這是一種需要強制執行的限制,因為虛函數調用機制的普遍實現都使用了一個大小固定的表,每個虛函數都對應表的一個入口。然而,成員函數模板的實例化個數,要等到整個程序都翻譯完畢才能確定,這就和表的大小(是固定的)發生了沖突。因此,如果(將來)要支持虛成員函數模板,將需要一種全新的C++編譯器和鏈接器機制。
- 相反,類模板的普通成員函數可以是虛函數,因為當類被實例化之后,它們的個數是固定的:
template <typename T> class Dynamic { public: virtual ~Dynamic(); // ok:類模板的普通成員函數,每個Dynamic只對應一個析構函數 template <typename T2> virtual void copy(T2 const&); // 錯誤,在確定Dynamic<T>實例的時候,也無法知道copy()的個數 };
8.1.2 模板的鏈接
1. 每個模板都必須有一個名字,而且在它所屬的作用域下,該名字必須是唯一的;除非函數模板可以被重載。特別是,類模板不能和另外一個實體共享一個名稱,這一點和class類型是不同的:
int C; class C; // 正確:類名稱和非類名稱位於不同的名字空間 int X; template <typename T> class X; // 錯誤:和變量X沖突 struct S; template <typename T> class S; // 錯誤:和struct S 沖突
2. 模板名字是具有鏈接的,當它們不能具有C鏈接。如下:
extern "C++" template <typename T> void normal(); // 這是缺省情況,上面的鏈接規范可以不寫 extern "C" template <typename T> void invalid(); // 錯誤的:模板不能具有C鏈接 extern "Xroma" template <typename T> void xroma_link(); // 非標准的,當某些編譯器將來可能支持寫Xroma語言的鏈接兼容性
3. 模板通常具有外部鏈接(參考博文 存儲持續性、作用域和鏈接性)。唯一的例外就是前面有static修飾符的名字空間作用域下的函數模板:
// external()函數模板 作為一個聲明,引用自 位於其他文件的、具有相同名稱的實體;
// 即引用位於其他文件的external()函數模板,也稱前置聲明 template <typename T> void external();
// internal函數模板(內部鏈接性) 與其他文件中具有相同名稱的模板沒有關系 template <typename T> static void internal();
因此我們知道(由於外部鏈接):不能在函數內部聲明模板[?TODO]。
8.1.3 基本模板
如果模板聲明的是一個普通聲明,我們就稱它聲明的是一個基本模板。
- 基本模板聲明是指:沒有在模板名稱后面添加一對尖括號(和里面實參)的聲明,如下:
template <typename T> class Box; template <typename T> void translate(T*);
- 顯然,當聲明局部特化的時候,聲明的就是非基本模板:template <typename T> class Point<T, int> {};
- 另外,函數模板必須是基本模板;
8.2 模板參數
現今存在3種模板參數:
- (1)類型參數(它們是使用最多的);
- (2)非類型參數 ;
- (3)模板的模板參數
注意:模板中,在同一對尖括號內部,位於后面的模板參數聲明可以引用前面的模板參數的名稱(但前面的不能引用后面的)。
8.2.1 類型參數
類型參數是通過關鍵字typename或者class引入的:它們兩者幾乎是等同的。
注意,在模板聲明內部,類型參數的作用類似於typedef(類型定義)名稱。例如,如果T是一個模板參數,就不能使用諸如class T 等形式的修飾名稱,即使T是一個要被class類型替換的參數也不可以:
template <typename Allocator> class List { class Allocator* allocator; // 錯誤 friend class Allocator; // Error };
8.2.2 非類型參數
1. 非類型參數表示的是:在編譯期或鏈接期可以確定的常值。這種參數的類型必須是下面的一種:
- (1)整型或者枚舉類型
- (2)指針類型(包含普通對象的指針類型、函數指針類型、指向成員的指針類型)
- (3)引用類型(指向對象或者指向函數的引用都是允許的)
所有其他的類型現今都不允許作為非類型參數使用。
2. 函數和數值類型也可以被指定為非模板參數,但要把它們先隱式地轉換為指針類型,這種轉型也稱為decay:
template <int buf[5]> class Lexer; // buf實際上是一個int*類型 template <int* buf> class Lexer; // 正確:這是上面的重新聲明
3. 非類型模板參數的聲明和變量的聲明很相似,但它們不能具有static、mutable等修飾符;只能具有const和volatile限定符。但如果這兩個限定符限定的是最外層的參數類型,編譯器將會忽略它們:
template <int const length> class Buffer; // 這里的const是沒用的,被忽略了
4. 最后,非類型模板參數只能是右值:它們不能被取址,也不能被賦值。
8.2.3 模板的模板參數
模板的模板參數是代表類模板的占位符(placeholder)。它的聲明和類模板的聲明很類似,但不能使用關鍵字struct和union:
template <template<typename X> class C> void f(C<int>* p);
模板的模板參數的參數可以具有缺省模板實參。如下:
template <template <typename T, typename A = MyAllocator> class Container > class Adaptation { Container<int> storage; // 隱式等同於Container<int, MyAllocator> ... };
注意:對於模板的模板參數而言,它的模板參數名稱只能被自身其他參數的聲明使用。如下:
template <template<typename T, T*> class Buf> class Lexer{ static char storage[5]; Buf<char, &Lexer<Buf>::storage[0]> buf; ... }; template <template <typename T> class List > class Node { static T* storage; // 錯誤:模板的模板參數的參數在這里不能被使用 };
注:由於通常模板的模板參數的參數名稱並不會在后面被用到,因此該參數也經常被省略不寫,即沒有命名。
8.2.4 缺省模板實參
- 1. 現今,只有類模板聲明才能具有缺省模板實參(函數模板不能具有缺省模板實參,但可以有缺省調用實參);
- 2. 與缺省的函數調用參數的約束一樣:對於任一個模板參數(比如T3),只有在T3之后的模板參數(比如T4、T5)都提供了缺省實參的前提下,才能具有缺省模板實參;
- 3. 后面的缺省值通常是在同個模板聲明中提供的,但也可以在前面的模板聲明中提供;如下:
template <typename T1, typename T2, typename T3, typename T4 = char, typename T5 = char> class Quintuple; //模板特化,其中 (T4,T5)缺省值由前面模板聲明提供 template <typename T1, typename T2, typename T3 = char, typename T4, typename T5> class Quintuple; // 正確,根據前面的模板聲明,T4和T5已經具有缺省值了 template <typename T1 = char, typename T2, typename T3, typename T4, typename T5> class Quintuple; // 錯誤:T1不能具有缺省實參,因為T2還沒有缺省實參
- 4. 缺省模板實參是從參數列后面向前檢查的,函數模板參數演繹是從參數列前面向后演繹的;
- 5. 缺省實參不能重復聲明:
template <typename T = void> class Value; template <typename T = void> class Value; // 錯誤:重復出現的缺省實參
8.3 模板實參
模板實參是指:在實例化模板時,用來替換模板參數的值。下面幾種機制可以來確定這些值:
- (1)顯式模板實參:緊跟在模板名稱后面,在一對尖括號內部的顯式模板實參值。所組成的整個實體稱為template-id。
- (2)注入式(injecter)類名稱:對於具有模板參數P1、P2……的類模板X,在它的作用域中,模板名稱(即X)等同於template-id:X<P1, P2, ……>【TODO】。
- (3)缺省模板實參:如果提供缺省模板實參的話,在類模板的實例中就可以省略顯式模板實參。然而,即使所有的模板參數都具有缺省值,一對尖括號還是不能省略的(即使尖括號內部為空,也要保留尖括號)。
- (4)實參演繹:對於不是顯式指定的函數模板實參,可以在函數的調用語句中,根據函數調用實參的類型來演繹出函數模板實參。
8.3.1 函數模板實參
- 顯式指定或者實參演繹;
- 注意,對於某些模板實參永遠也得不到演繹的機會(比如函數返回值類型);
- 於是我們最好把這些實參所對應的參數放在模板參數列表的開始處(注意是開始處,和缺省模板參數的放置位置順序相反:為了顯式指定模板參數T,需要把T放到參數列表最前;為了提供T的缺省模板實參,需要確保參數列表中位於T后面的模板參數也都提供了缺省實參),從而可以顯式指定這些參數,而其他參數仍可以進行實參演繹。如下:
template <typename RT, typename T> inline RT func(T const &){ ... } template <typename T, typename RT> inline RT func1(T const &){ ... } int main() { double value = func<double>(-1); // double 顯式指定RT類型,-1實參演繹T類型 double value = func1<int, double>(-1); // 要顯式指定RT的類型,必須同時顯式指定它前面的類型T }
由於函數模板可以被重載,所以對於函數模板而言,顯式提供所有的實參並不足以標識每一個函數:在一些例子中,它標識的是由許多函數組成的函數集合。如下:
template <typename Func, typename T> void apply(Func func_ptr, T x) { func_ptr(x); } template <typename T> void single(T); template <typename T> void multi(T); template <typename T> void multi(T*); int main() { apply(&single<int>, 3); // 正確 apply(&multi<int>, 7); // 錯誤:multi<int> 不唯一 }
注:&multi<int>可以是兩種函數類型中的任意一種,產生二義性,不能演繹出Func的類型。
另外,在函數模板中,顯式指定模板實參可能會試圖構造一個無效的C++類型。如下:
template<typename T> RT1 test(typename T::X const*); template<typename T> TR2 test(...);
表達式test<int>可能會使第1個函數模板毫無意義,因為基本int類型根本就沒有成員類型X.
顯然,“替換失敗並非錯誤(substitution-failure-is-not-an-error, SFINAE)”原則是令函數模板可以重載的重要因素。
SFINAE原則保護的只是:允許試圖創建無效的類型,但並不允許試圖計算無效的表達式。如:
template<int N> int g() { return N; } template<int* P> int g() { return *P; } int main() { return g<1>(); // 雖然數字1不能被綁定到int*參數,但是應用了SFINAE原則 }
8.3.2 類型實參
模板的類型實參是一些用來指定模板類型參數的值。我們平時使用的大多數類型都可以被用作模板的類型實參。但有兩種情況例外:
(1)局部類和局部枚舉(換句話說,指在函數定義內部聲明的類型)不能作為模板的類型實參;
(2)未命名的class類型或者未命名的枚舉類型不能作為模板的類型實參(然而,通過typedef聲明給出的未命名類和枚舉是可以作為模板類型實參的)。
template <typename T> class List { ... }; typedef struct { double x, y, z; }Point; typedef enum { red, green, blue } *ColorPtr; int main() { struct Association { int* p; int* q; }; List<Association*> error1; // 錯誤:模板實參中使用了局部類型 // 錯誤:模板實參中使用了未命名的類型,因為typedef定義的是*ColorPtr,並非ColorPtr List<ColorPtr> error2; List<Point> ok; // 正確:通過使用typedef定義的未命名類型 }
8.3.3 非類型實參
非類型模板實參是那些替換非類型參數的值。這個值必須是以下幾種中的一種:
(1)某一個具有正確類型的非類型模板參數;
(2)一個編譯期整型常值(或枚舉值);這只有在參數類型和值的類型能夠進行匹配,或者值的類型可以隱式地轉換為參數類型的前提下,才是合法的。
(3)前面有單目運算符&(即取址)的外部變量或者函數的名稱。對於函數或數組變量,&運算符可以省略。這類模板實參可以匹配指針類型的非類型參數。
(4)對於引用類型的非類型模板參數,前面沒有&運算符的外部變量和外部函數也是可取的;
(5)一個指向成員的指針常量;換句話說,類似&C::m的表達式,其中C是一個class類型,m是一個非靜態成員(成員變量或者函數)。這類實參只能匹配類型為“成員指針”的非類型參數。
當實參匹配“指針類型或者引用類型的參數”時,用戶定義的類型轉換(例如單參數的構造函數和重載類型轉換運算符)和由派生類到基類的類型轉換,都是不會被考慮的;即使在其他的情況下,這些隱式類型指針是有效的,但在這里都是無效的。隱式類型轉換的唯一應用只能是:給實參加上關鍵字const或者volatile。
template <typename T, T nontyep_param> class C; C<int, 33>* c1; // 整型 int a; C<int*, &a>* c2; // 外部變量的地址 void f(); void f(int); C<void(*)(int), f>* c3; // 函數名稱:在這個例子中,重載解析會選擇f(int),f前面的&隱式省略了 class X { public: int n; static bool b; }; C<bool&, X::b>* c4; // 靜態成員是可取的變量(和函數)名稱 C<int X::*, &X::n>* c5; // 指向成員的指針常量 template<typename T> void templ_func(); C<void(), &templ_func<double> >* c6; // 函數模板實例同時也是函數
注意:模板實參的一個普遍約束是:在程序創建的時候,編譯器或者鏈接器要能夠確定實參的值。如果實參的值要等到程序運行時才能夠確定(譬如,局部變量的地址),就不符合“模板是在程序創建的時候進行實例化”的概念了。(模板實參是一個在編譯期可以確定的值,這樣才符合“模板是在程序創建的時候進行實例化”的概念。)
另一方面,有些常值不能作為有效的非類型實參:空指針常量、浮點型值、字符串。
template <typename T, T nontyep_param> class C; class Base { public: int i; }base; class Derived:public Base { }derived_obj; C<Base*, &derived_obj>* err1; // 錯誤:這里不會考慮派生類到基類的類型轉換 C<int&, base.i>* err2; // 錯誤:域運算符(.)后面的變量不會被看成變量 int a[10]; C<int*, &a[10]>* err3; // 錯誤:單一數組元素的地址並不是可取的
8.3.4 模板的模板實參
1. “模板的模板實參”必須是一個類模板,它本身具有參數,該參數必須精確匹配它“所替換的模板的模板參數”本身的參數。在匹配過程中,“模板的模板實參” 的缺省模板實參將不會被考慮(但是如果“模板的模板參數”具有缺省實參,那么模板的實例化過程是會考慮模板的模板參數的缺省實參的)。
template <typename T1, typename T2, template<typename T, typename = std::allocator<T> > class Container> // Container現在就能夠接受一個標准容器模板了 class Relation { public: ... private: Container<T1> dom1; Container<T2> dom2; };
2. 從語法上講,只有關鍵字class才能被用來聲明模板的模板參數;但是這並不意味只有用關鍵字class聲明的類模板才能作為它的替換實參。實際上,“struct模板”、“union模板”都可以作為模板的模板參數的有效實參。這和我們前面所提到的事實很相似:對於用關鍵字class聲明的模板類型參數,我們可以用(滿足約束的)任何類型作為它的替換實參。
8.3.5 實參的等價性
當每個對應實參值都相等時,我們就稱這兩組模板實參是相等的。對於類型實參,typedef名稱並不會對等價性產生影響;就是說,最后比較的還是typedef原本的類型。對於非類型的整型實參,進行比較的是實參的值;至於這些值是如何表達的,也不會產生影響。
另外,從函數模板產生(即實例化出來)的函數一定不會等於普通函數,即便這兩個函數具有相同的類型和名稱。這樣,針對類成員,我們可以引申出兩點結論:
(1)從成員函數模板產生的函數永遠也不會改寫一個虛函數(進一步說明成員函數模板不能是一個虛函數)。(虛函數表有固定大小)。
(2)從構造函數模板產生的構造函數一定不會是缺省的拷貝構造函數(類似,從賦值運算符模板產生的賦值運算符也一定不會是一個拷貝賦值運算符。但是,后面這種情況通常不會出現問題,因為與拷貝構造函數不同的是:賦值運算符永遠也不會被隱式調用)。
8.4 友元
友元聲明的基本概念是很簡單的:授予“某個類或者函數訪問友元聲明所在的類”的權利。然而,由於以下兩個事實,這些簡單概念卻變得有些復雜:
(1)友元聲明可能是某個實體的唯一聲明;
(2)友元函數的聲明可以是一個定義。
友元類的聲明不能是類定義,因此友元類通常都不會出現問題。在引入模板之后,友元類聲明的唯一變化只是:可以命名一個特定的類模板實例為友元。
template <typename T> class Node; template <typename T> class Tree { friend class Node<T>; ... };
顯然,如果要把類模板的實例聲明為其他類(或者類模板)的友元,該類模板在聲明的地方必須是可見的(這里的意思是要求類模板有前置聲明或者聲明出能看到的定義,因為類模板從T實例化出來的實體會根據類模板的定義來考量類型T是否提供了所需要的操作)。然而對於一個普通類,就沒有這個要求:
template <typename T> class Tree { friend class Factory; // 正確:即使這里是Factory的首次聲明 friend class Node<T>; // 如果Node在此是不見見的,這條語句就是錯誤的 };
8.4.1 友元函數
通過確認緊接在友元函數名稱后面的是一對尖括號,我們可以把函數模板的實例聲明為友元。
template <typename T1, typename T2> void combine(T1, T2); class Mixer { friend void combine<>(int&, int&); // 正確:T1 = int&, T2 = int& friend void combine<int, int>(int, int); // 正確:T1 = int, T2 = int friend void combine<char>(char, int); // 正確:T1 = char, T2 = int friend void combine<char>(char&, int&); // 錯誤:不能匹配上面的combine()模板 friend void combine<>(long, long){ ... }; // 錯誤:這里的友元聲明不允許出現定義 };
另外應該知道:我們不能再友元聲明中定義一個模板實例(我們最多只能定義一個特化);因此,命名一個實例的友元聲明是不能作為定義的。
如果名稱后面沒有緊跟一對尖括號,那么只有在下面兩種情況下是合法的:
(1)如果名稱不是受限的(就是說,沒有包含一個形如雙冒號的域運算符),那么該名稱一定不是(也不能)引用一個模板實例。如果在友元聲明的地方,還看不到所匹配的非模板函數(即普通函數),那么這個友元聲明就是函數的首次聲明。於是,該聲明可以是定義。(非受限、首次聲明——可以定義)
(2)如果名稱是受限的(就是說前面有雙冒號::),那么該名稱必須引用一個在此之前聲明的函數或者函數模板。在匹配的過程中,匹配的函數要優先於匹配的函數模板。然而,這樣的友元聲明不能是定義。(受限——不能定義)
void multiply(void*); // 普通函數 template <typename T> void multiply(T); // 函數模板 class Comrades { friend void multiply(int) { } // 定義了一個新的函數::multiply(int),非受限函數名稱,不能引用模板實例 friend void ::multiply(void*) // 引用上面的普通函數,不會引用multiply<void*>實例 friend void ::multiply(int); // 引用一個模板實例,通過實參演繹 friend void ::multiply<double*>(double*) // 受限名稱還可以具有一對尖括號,但模板在此必須是可見的 friend void ::error() { } // 錯誤:受限的友元不能是一個定義 };
在前面的例子中,我們是在一個普通類里面聲明友元函數。如果需要在類模板里面聲明友元函數,前面的規則仍然是適用的,唯一的區別就是:可以使用模板參數來標志友元函數。
template <typename T> class Node { Node<T>* allocate(); .... }; template <typename T> class List { friend Node<T>* Node<T>::allocate(); };
然而,如果我們在類模板中定義一個友元函數,那么將會出現一個很有趣的現象。因為對於任何只在模板內部聲明的實體,都要等到模板被實例化之后,才回是一個具體的實體;在這之前該實體是不存在的。類模板的友元函數也是如此。
template <typename T> class Creator { friend void appear() { ... } //一個新函數::appear(),但要等到Creator被實例化之后才存在 }; Creator<void> miracle; // 這時才生成::appear() Creator<double> oops; // 錯誤:::appear()第2次被生成
在這個例子中,兩個不同的實例化過程生成了兩個完全相同的定義(即appear函數),這違反了ODR原則。
因此,我們必須確定:在模板內部定義的友元函數的類型定義中,必須包含類模板的模板參數(除非我們希望在一個特定的文件中禁止多於一個的實例被創建,但這種用法很少)。修改上面代碼如下:
template <typename T> class Creator { friend void feed(Creator<T>*) { ... } //每個T都生成一個不同的 ::feed()函數 }; Creator<void> miracle; // 生成 ::feed(Creator<void>*) Creator<double> oops; // 生成 ::feed(Creator<double>*)
在這個例子中,每個Creator的實例都生成了一個不同的feed()函數。另外我們應該知道:盡管這些函數是作為模板的一部分被生成的,但函數本身仍然是普通函數,而不是模板的實例。最后一點就是:由於函數的實體處於類定義的內部,所以這些函數是內聯函數。因此,在兩個不同的翻譯單元中可以生成相同的函數。
8.4.2 友元模板
我們通常聲明的友元只是:函數模板的實例或者類模板的實例,我們指定的友元也只是特定的實體。然而,我們有時候需要讓模板的所有實例都成為友元,這就需要聲明友元模板:
class Manager { template <typename T> friend class Task; template <typename T> friend void Schedule<T>::dispatch(Task<T>*); template <typename T> friend int ticket() { return ++Manager::counter; }; static int counter; };
和普通友元的聲明一樣,只有在友元模板聲明的是一個非受限的函數名稱,並且后面沒有緊跟尖括號的情況下,該友元模板聲明才能成為定義。
友元模板聲明的只是基本模板和基本模板的成員。當進行這些聲明之后,與該基本模板相對於的模板局部特化和顯式特化都會被自動地看成友元。
template <typename T> class Box; template <typename T> void translate(T*);
