目錄
23.1? 引言和概觀(Introduction and Overview)
23.2? 一個簡單的字符串模板(A Simple String Template)
23.2.1? 模板的定義(Defining a Template)
23.2.2? 模板實例化(Template Instantiation)
23.3? 類型檢查(Type Checking)
23.3.1? 類型等價(Type Equivalence)
23.3.2? 錯誤檢測(Error Detection)
23.4? 類模板成員(Class Template ?Members)
23.4.1? 數據成員(Data Members)
23.4.2? 成員函數(Member? Functions)
23.4.3? 類成員另名(Member? Type Aliases)
23.4.4? static成員另名(static Members)
23.4.5? 成員類型(Member Types)
23.4.6? 成員模板(Member Template)
23.4.6.1? 模板和構造函數(Templates and Constructors)
23.4.6.2? 模板和virtual(Templates and virtual)
23.4.6.3 ?使用嵌套(Use of Nesting)
23.4.7? 友元(Friends)
23.5? 函數模板 (Function Templates ?)
23.5.1? 函數模板參數(Function Template Arguments)
23.5.2? 函數模板參數推導(Function Template Arguments Deduction)
23.5.2.1? 參考推導(Reference Deduction)
23.5.3? 函數模板重載(Function Template Overloading)
23.5.3.1? 歧義消除(Ambiguity Resolution)
23.5.3.2? 參數替換失敗(Argument Substitution Failure)
23.5.3.3? 重載和派生(Overloading and Derivation)
23.5.3.4? 重載和非推導參數(Overloading and Non-Deduced Parameters)
23.6? 模板別名 (Template Aliases )
23.7? 源碼組織 (Source Code Organization)
23.7.1? 鏈接(Linkage)
23.8? 建議 (Advice)
模板(Template)
23.1? 引言和概觀(Introduction and Overview)
??? 模板以使用類型作為參數的編程形式直接支持泛型編程(§3.4)。C++ 模板機制允許類型或值作為類、函數或類型別名定義中的參數。模板提供了一種表示各種通用概念的直接方法以及將它們組合起來的簡單方法。生成的類和函數在運行時和空間效率方面可以與手寫的、不太通用的代碼相匹配。
??? 模板僅依賴于其參數類型中實際使用的屬性,并且不要求用作參數的類型顯式相關。特別是,用于模板的參數類型不必是繼承層級結構的一部分。內置類型是可以接受的,并且非常常見于模板參數。
模板提供的組合是類型安全的(任何對象都不能以與其定義不一致的方式隱式使用),但遺憾的是,模板對其參數的要求不能簡單直接地在代碼中說明(§24.3)。Explosion
每一個主要的標準庫抽象形式都表示為一個模板(例如,string、ostream、regex、compex、list、map、unique_ptr、thread、future、tuple和function),關鍵操作也是如此(例如,string比較、(控制臺)輸出運算符 <<、compex算術運算、list插入和刪除以及 sort())。這使得本書的庫章節(第 IV 部分)成為依賴它們的模板和編程技術的豐富示例來源。
這里介紹模板,主要關注設計、實現和使用標準庫所需的技術。標準庫比大多數軟件需要更高程度的通用性、靈活性和效率。因此,可用于設計和實現標準庫的技術在設計各種問題的解決方案時都是有效和高效的。這些技術使實現者能夠將復雜的實現隱藏在簡單的界面后面,并在用戶有特定需求時向用戶展示復雜性。
模板及其基本使用技巧是本章及后面六章的重點。本章重點介紹最基本的模板功能和使用它們的基本編程技巧:
§23.2 簡單字符串模板:通過字符串模板示例介紹定義和使用類模板的基本機制。
§23.3 類型檢查:適用于模板的類型等價性和類型檢查的基本規則。
§23.4 類模板成員:如何定義和使用類模板的成員。
§23.5 函數模板:如何定義和使用函數模板。如何解決函數模板和普通函數的重載問題。
§23.6 模板別名:模板別名提供了一種強大的機制,用于隱藏實現細節并清理用于模板的符號。
§23.7 源代碼組織:如何將模板組織到源文件中。
第 24 章“泛型編程”介紹了泛型編程的基本技術,并探討了概念(對模板參數的要求)的基本思想:
§24.2 算法和提升:從具體示例開發泛型算法的基本技術示例。
§24.3 Concepts(布爾謂詞):介紹并討論?concept?的基本概念(notion),即模板可以對其模板參數施加的一組要求。
§24.4 使概念具體化:介紹使用以編譯時謂詞表示的概念的技術。
第 25 章“特化”討論了模板參數傳遞和具體化的概念:
§25.2 模板參數和參數:什么可以作為模板參數:類型、值和模板。如何指定和使用默認模板參數。
§25.3 特化:針對特定一組模板參數的模板的特殊版本(稱為特化)可以由編譯器從模板生成,也可以由程序員提供。
第 26 章? 實例化介紹了與模板特化(實例)生成和名稱綁定相關的問題:
§26.2 模板實例化:編譯器何時以及如何從模板定義生成特化以及如何手動指定它們的規則。
§26.3 名稱綁定:確定模板定義中使用的名稱引用哪個實體的規則。
第 27 章 模板和層級結構討論了模板支持的通用編程技術與類層級結構支持的面向對象技術之間的關系。重點是如何結合使用它們:
§27.2 參數化和層級結構:模板和類層次結構是表示相關抽象集的兩種方式。我們如何在它們之間進行選擇?
§27.3 類模板的層級結構:為什么簡單地將模板參數添加到現有的類層次結構通常不是一個好主意?
§27.4 模板參數作為基類:介紹為類型安全和性能而組合接口和數據結構的技術。
第 28 章“元編程”集中介紹了模板作為生成函數和類的手段的使用:
§28.2 類型函數:以類型為參數或返回類型為結果的函數。
§28.3 編譯時控制結構:如何表達類型函數的選擇和遞歸,以及一些使用它們的經驗法則。
§28.4 條件定義:enable_if:如何使用(幾乎)任意謂詞有條件地定義函數和重載模板。
§28.5 編譯時列表:元組:如何構建和訪問具有(幾乎)任意類型元素的列表。
§28.6 可變參數模板:如何(以靜態類型安全的方式)定義采用任意類型的任意數量模板參數的模板。
§28.7 SI單位示例:此示例結合使用簡單的元編程技術與其他編程技術來提供一個計算庫,這些計算庫(在編譯時)檢查是否正確使用了米、千克和秒單位制。
第 29 章“矩陣設計”演示了如何結合使用各種模板功能來解決具有挑戰性的設計任務:
§29.2 矩陣模板:如何定義具有靈活且類型安全的初始化、訂閱和子矩陣的 N 維矩陣。
§29.3 矩陣算術運算:如何在 N 維矩陣上提供簡單的算術運算。
§29.4 矩陣實現:一些有用的實現技術。
§29.5 求解線性方程:簡單矩陣使用的一個例子。
模板在本書早期就已引入(§3.4.1,§3.4.2)并使用,所以我假設你對它們有一定的熟悉。
23.2? 一個簡單的字符串模板(A Simple String Template)
??? 考慮一個字符串。字符串是一個包含字符并提供下標、連接和比較等操作的類,我們通常將這些操作與“字符串”的概念聯系起來。我們希望為許多不同類型的字符提供這種行為。例如,有符號字符、無符號字符、中文字符、希臘字符等的字符串在各種情況下都很有用。因此,我們希望以對特定字符類型的最小依賴來表示“字符串”的概念。字符串的定義依賴于字符可以復制的事實,僅此而已(§24.3)。因此,我們可以通過從§19.3 中獲取 char 字符串并將字符類型作為參數來創建更通用的字符串類型:
template<typename C>
class String {
public:
String();
explicit String(const C?);
String(const String&);
String operator=(const String&);
// ...
C& operator[](int n) { return ptr[n]; } // 非驗證元素訪問
String& operator+=(C c); // c加到字符串尾
// ...
private:
static const int short_max = 15; // 為了短字符串優化
int sz;
C? ptr; // 指向 sz 大小的 C 指針。
};
template<typename C> 前綴指定聲明的模板,并且將在聲明中使用類型參數 C 。引入后,C 的使用方式與其他類型名稱完全相同。C 的范圍擴展到以 template<typename C> 為前綴的聲明的末尾。您可能更喜歡更短且等效的形式 template<class C>。無論哪種情況,C 都是類型名稱;它不必是類的名稱。數學家會將 template<typename C> 識別為傳統的“對于所有 C”的變體,或者更具體地說是“對于所有類型 C”或甚至“對于所有 C,使得 C 是一種類型”。如果按照這個思路思考,您會注意到 C++ 缺乏一個完全通用的機制來指定模板參數 C 的所需屬性。也就是說,我們不能說“對于所有 C,使得 ...”,其中“...”是一組對 C 的要求。換句話說,C++ 沒有提供直接的方式來說明模板參數 C 應該是哪種類型(§24.3)。
??? 類模板的名稱后接一個用 < > 括起來的類型,是類的名稱(由模板定義),可以像其他類名一樣使用。例如:
String<char> cs;
String<unsigned char> us;
String<wchar_t> ws;
struct Jchar { /* ... */ }; // Japanese character
String<Jchar> js;
除了其名稱的特殊語法外,String<char> 的工作方式與使用 §19.3 中的 String 類定義完全相同。將 String 設為模板允許我們為任何類型字符的 String 的 char 提供我們已有的 String 的功能。例如,如果我們使用標準庫 map 和 String 模板,§19.2.1 中的字數統計示例將變為:
int main() // 基于輸入統計每一個單詞出現的次數
{
map<String<char>,int> m;
for (String<char> buf; cin>>buf;)
++m[buf];
// ... 輸出結果 ...
}
我們的日文類型的版本 Jchar 將是:
int main() // 基于輸入統計每一個單詞出現的次數
{
map<String<Jchar>,int> m;
for (String<Jchar> buf; cin>>buf;)
++m[buf];
// ... 輸出結果 ...
}
標準庫提供了與模板化的 String(§19.3,§36.3)類似的模板類 basic_string。在標準庫中,string 是 basic_string<char>(§36.3)的同義詞:
??? using string = std::basic_string<char>;
這使得我們可以編寫如下字數統計程序:
int main() // 基于輸入統計每一個單詞出現的次數
{
map<string,int> m;
for (string buf; cin>>buf;)
++m[buf];
// ... 輸出結果 ?...
}
一般來說,類型別名(§6.5)對于縮短從模板生成的類的長名稱很實用。另外,我們通常不愿意知道類型定義的細節,而別名可以讓我們隱藏類型是從模板生成的這一事實。
23.2.1? 模板的定義(Defining a Template)
從類模板生成的類是完全普通的類。因此,使用模板并不意味著任何超出等效“手寫”類所使用的運行時機制。事實上,使用模板可以減少生成的代碼,因為只有在使用該成員時才會生成類模板成員函數的代碼(§26.2.1)。
??? 除了類模板,C++ 還提供函數模板(§3.4.2,§23.5)。我將在類模板的上下文中介紹模板的大部分“機制”,并將函數模板的詳細討論推遲到 §23.5。模板是關于如何在給定合適模板參數的情況下生成某些內容的規范;用于執行該生成的語言機制(實例化(§26.2)和特化(§25.3))并不關心生成的是類還是函數。因此,除非另有說明,否則模板規則同樣適用于類模板和函數模板。模板也可以定義為別名(§23.6),但不提供其他合理的構造,例如命名空間模板。
??? 有些人會在類模板(class template)和模板類(template class)這兩個術語之間做出語義上的區分。我不會這樣做;那樣太過微妙:請考慮這兩個術語可以互換。同樣,我認為函數模板(function template)可以與模板函數(template function)互換。
??? 在設計類模板時,在將特定類(例如 String)轉換為模板(例如 String<C>)之前對其進行調試通常是一個好主意。通過這樣做,我們可以在具體示例的上下文中處理許多設計問題和大多數代碼錯誤。所有程序員都熟悉這種調試,大多數人處理具體示例比處理抽象概念更好。之后,我們可以處理可能因泛化而產生的任何問題,而不會被更常見的錯誤所困擾。同樣,在嘗試理解模板時,在嘗試完全理解模板的通用性之前,想象它對特定類型參數(例如 char)的行為通常很有用。這也符合這樣的理念:通用組件應該作為一個或多個具體示例的泛化來開發,而不是簡單地從第一原理進行設計(§24.2)。
??? 類模板的成員的聲明和定義與非模板類完全相同。模板成員不需要在模板類本身內定義。在這種情況下,必須在其他地方提供其定義,就像非模板類成員一樣(§16.2.1)。模板類的成員本身就是由其模板類的參數參數化的模板。當此類成員在其類之外定義時,必須將其明確聲明為模板。例如:
template<typename C>
String<C>::String() // String<C>的構造函數
:sz{0}, ptr{ch}
{
ch[0] = {}; // 合適字符類型的終止符 0
}
template<typename C>
String& String<C>::operator+=(C c)
{
// ... 在字符串尾追加字符c
return ?this;
}
模板參數(例如 C )是參數,而不是特定類型的名稱。但是,這并不影響我們使用名稱編寫模板代碼的方式。在 String< C > 的作用域內,使用 < C > 限定對于模板本身的名稱來說是多余的,因此 String< C >::String 是構造函數的名稱。
??? 正如程序中只能有一個函數定義類成員函數一樣,程序中也只能有一個函數模板定義類模板成員函數。但是,特化(§25.3)使我們能夠在給定特定模板參數的情況下為模板提供替代實現。對于函數,我們還可以使用重載為不同的參數類型提供不同的定義(§23.5.3)。
??? 無法重載類模板名稱,因此如果在某個范圍內聲明了類模板,則不能在該范圍內聲明其他具有相同名稱的實體。例如:
template<typename T>
class String { /* ... */ };
class String { /* ... */ }; // 錯 :定義兩次
用作模板參數的類型必須提供模板所需的接口。例如,用作 String 參數的類型必須提供通常的復制操作(§17.5,§36.2.2)。請注意,不要求同一模板參數(parameter)的不同參數(arguments)應通過繼承關聯。另請參閱 §25.2.1(模板類型參數),§23.5.2(模板參數推導)和 §24.3(模板參數要求)。
23.2.2? 模板實例化(Template Instantiation)
??? 從模板加上模板參數列表生成類或函數的過程通常稱為模板實例化(§26.2)(譯注:創建類或函數對象)。指定了模板參數列表的模板版本稱為特化(譯注:實例化時指定具體參數類型,例如 int)。
??? 一般來說,確保為每個使用的模板參數列表生成模板的特化是實現的工作(而不是程序員的工作)。例如:
String<char> cs; //注:生成對像 cs 稱為模板實例化,指定具體類型char稱為特化
void f()
{
String<Jchar> js;
cs = "It's the implementation's job to figure out what code needs to be generated";
}
為此,實現產生了類 String<char> 和 String<Jchar>、它們的析構函數和默認構造函數以及 String<char>::operator=(char?) 的聲明。其他成員函數未使用,也不會生成。生成的類是完全普通的類,它們遵循類的所有常規規則。同樣,生成的函數是普通函數,它們遵循函數的所有常規規則。
??? 顯然,模板提供了一種從相對較短的定義生成大量代碼的強大方法。因此,需要謹慎行事,以避免幾乎相同的函數定義 (§25.3) 充斥內存。在另一方面,可以編寫模板來實現生成代碼無法實現的質量。特別是,使用模板與簡單內聯相結合的組合可用于消除許多直接和間接函數調用。例如,這就是在高度參數化的庫中將關鍵數據結構的簡單操作(例如 sort() 中的 < 和矩陣計算中標量的 + ) 減少為單個機器指令的方式。因此,不謹慎使用模板導致生成非常相似的大型函數會導致代碼膨脹,而使用模板來啟用微小函數的內聯可以導致與其他方法相比代碼顯著縮減(和加速)。具體來說,為簡單的 < 或 [] 生成的代碼通常是單個機器指令,它比任何函數調用都快得多,并且比調用函數并接收其結果所需的代碼要小得多。
23.3? 類型檢查(Type Checking)
??? 模板實例化采用模板和一組模板參數并從中生成代碼。由于實例化時有如此多的信息可用,因此將模板定義和模板參數類型的信息編織在一起提供了極大的靈活性,并可以產生無與倫比的運行時性能。遺憾的是,這種靈活性也意味著類型檢查的復雜性和準確報告類型錯誤的困難。
??? 類型檢查是在模板實例化生成的代碼上進行的(就像程序員手動擴展模板一樣)。生成的代碼可能包含許多模板的用戶從未聽說過的內容(例如模板實現的詳細信息的名稱),并且通常在構建過程的后期才會出現。程序員看到/寫的內容與編譯器類型檢查的內容之間的不匹配可能是一個大問題,我們需要設計我們的程序以盡量減少其后果。
??? 模板機制的根本弱點在于無法直接表達對模板參數的要求。例如,我們不能說:
template<Container Cont, typename Elem>
requires Equal_comparable<Cont::value_type ,Elem>() //要求類型 types和Elem
int find_index(Cont& c, Elem e); // 在 c 中查找 e 的索引
即,在 C++ 中,我們無法直接說 Cont 應該是一種可以充當容器的類型,而 Elem 類型應該是一種允許我們將值與 Cont 元素進行比較的類型。我們正在努力在未來版本的 C++ 中實現這一點(不會損失靈活性,不會損失運行時性能,也不會顯著增加編譯時間 [Sutton,2011]),但現在我們只能放棄。
??? 有效處理與模板參數傳遞相關的問題的第一步是建立一個討論需求的框架和詞匯表。將一組模板參數要求視為謂詞。例如,我們可以將“C 一定是容器”視為一個謂詞(predicate)(譯注:即下一個判斷),它以類型 C 作為參數,如果 C 是容器(但是我們可能已經定義了“容器”)則返回 true,如果不是則返回 false。例如,Container<vector<int>>() 和 Container<list<string>>() 應該為 true,而 Container<int>() 和 Container<shared_ptr<string>>() 應該為 false。我們將這樣的謂詞稱為(基于模板參數的)concept?。布爾謂詞在 C++ 中還不是語言構造;它是一個我們可以用于推理基于模板參數的要求的概念(notion),在注釋中使用,有時還可以用我們自己的代碼來支持(§24.3)。
??? 首先,將概念視為設計工具:將 Container<T>() 指定為一組注釋,說明類型 T 必須具有哪些屬性才能使 Container<T>() 為真。例如:
? T 必須具有下標運算符 ([])。
? T 必須具有 size() 成員函數。
? T 必須具有成員類型 value_type,即其元素的類型。
??? 請注意,此列表不完整(例如,[] 將什么作為參數,它返回什么?)并且未能解決大多數語義問題(例如,[] 實際上做什么?)。但是,即使是部分要求也可能很有用;即使是部分要求也可能很有用;即使是非常簡單的東西,我們也可以手動檢查我們的用法并發現明顯的錯誤。例如,Container<int>() 顯然是錯誤的,因為 int 沒有下標運算符。我將回到概念的設計(§24.3),考慮在代碼中支持概念的技術(§24.4),并給出一組有用概念的示例(§24.3.2)。現在,請注意,C++ 不直接支持概念,但這并不意味著概念不存在:對于每個工作模板,設計者都會為其參數考慮一些概念。Dennis Ritchie有句名言:“C 是一種強類型、弱檢查的語言。”你也可以對 C++ 的模板說同樣的話,只是模板參數要求(布爾謂詞)的檢查實際上是完成了的,但它在編譯過程中完成得太晚了,而且抽象程度太低,沒有幫助。
23.3.1? 類型等價(Type Equivalence)
??? 給定一個模板,我們可以通過提供模板參數來生成類型。例如:
String<char> s1;
String<unsigned char> s2;
String<int> s3;
using Uchar = unsigned char;
using uchar = unsigned char;
String<Uchar> s4;
String<uchar> s5;
String<char> s6;
template<typename T, int N> // §25.2.2
class Buffer;
Buffer<String<char>,10> b1;
Buffer<char,10> b2;
Buffer<char,20?10> b3;
當對模板使用同一組模板參數時,我們總是引用相同的生成類型。但是,在這種情況下,“相同”是什么意思?別名不會引入新類型,因此 String<Uchar> 和 String<uchar> 與 String<unsigned char> 是同一類型。相反,因為 char 和 unsigned char 是不同的類型(§6.2.3),所以 String<char> 和 String<unsigned char> 是不同的類型。
??? 編譯器可以評估常量表達式(§10.4),因此 Buffer<char,20-10> 被識別為與Buffer<char,10> 相同的類型。
??? 通過不同的模板參數從單個模板生成的類型是不同的類型。特別是,從相關參數生成的類型不會自動關聯。例如,假設 Circle 是一種 Shape:
Shape? p {new Circle(p,100)}; //Circle* 轉換為 Shape*
vector<Shape>? q {new vector<Circle>{}}; // 錯: 無 vector<Circle>* 向 vector<Shape>* 的轉換
vector<Shape> vs {vector<Circle>{}}; // 錯: 無 vector<Circle> 到 vector<Shape> 的轉換
vector<Shape?> vs {vector<Circle?>{}}; // 錯: 無 vector<Circle*> 到 、、vector<Shape*> 的轉換
如果允許此類轉換,則會導致類型錯誤(§27.2.1)。如果需要在生成的類之間進行轉換,程序員可以定義它們(§27.2.2)。
23.3.2? 錯誤檢測(Error Detection)
??? 模板定義后,會與一組模板參數結合使用。定義模板時,會檢查定義中的語法錯誤,還可能檢查可以獨立于特定模板參數集檢測的其他錯誤。例如:
template<typename T>
struct Link {
Link? pre;
Link?suc //語法錯誤: 缺失分號
T val;
};
template<typename T>
class List {
Link<T>? head;
public:
List() :head{7} { } // 錯誤: 用 int 初始化指針
List(const T& t) : head{new Link<T>{0,o,t}} { } // 錯: 未定義修飾符 o
// ...
void print_all() const;
};
編譯器可以在定義時或稍后使用時捕獲簡單的語義錯誤。用戶通常更喜歡早期檢測,但并非所有“簡單”錯誤都易于檢測。在這里,我犯了三個“錯誤”:
? 一個簡單的語法錯誤:在聲明末尾遺漏了一個分號。
? 一個簡單的類型錯誤:無論模板參數是什么,指針都不能用整數 7 初始化。
? 名稱查找錯誤:標識符 o(當然錯誤輸入 0 了 )不能作為 Link<T> 構造函數的參數,因為作用域中沒有這樣的名稱。
模板定義中使用的名稱必須在作用域內,或者以某種合理明顯的方式依賴于模板參數(§26.3)。依賴模板參數 T 的最常見和最明顯的方式是明確使用名稱 T、使用 T 的成員以及采用類型 T 的參數。例如:
template<typename T>
void List<T>::print_all() const
{
for (Link<T>? p = head; p; p=p?>suc) // p 依賴 T
cout << ?p; //<< 依賴 T
}
與模板參數使用相關的錯誤只有在使用模板時才能檢測到。例如:
class Rec {
string name;
string address;
};
void f(const List<int>& li, const List<Rec>& lr)
{
li.print_all();
lr.print_all();
}
li.print_all() 檢查無誤,但 lr.print_all() 給出類型錯誤,因為 Rec 沒有定義 << 輸出運算符。最早可以檢測到的與模板參數相關的錯誤是在模板首次用于特定模板參數時。該點稱為實例化的第一個點 (§26.3.3)。允許實現推遲到基本上所有檢查之后,直到程序鏈接,對于某些錯誤,鏈接時間也是可以進行完整檢查的最早時間點。無論何時進行檢查,都會檢查同一組規則。自然,用戶更喜歡早期檢查。
23.4? 類模板成員(Class Template ?Members)
與類完全一樣,模板類可以有多種類型的成員:
? 數據成員(變量和常量);§23.4.1
? 成員函數;§23.4.2
? 成員類型別名;§23.6
? 靜態成員(函數和數據);§23.4.4
? 成員類型(例如,成員類);§23.4.5
? 成員模板(例如,成員類模板);§23.4.6.3
此外,類模板可以聲明友函數,就像“普通類”一樣;§23.4.7。
??? 類模板成員的規則就是其生成的類的規則。也就是說,如果你想知道模板成員的規則是什么,只需查找普通類成員的規則(第 16 章,第 17 章和第 20 章);這將回答大多數問題。
23.4.1? 數據成員(Data Members)
對于“普通類”,類模板可以具有任何類型的數據成員。非 static 數據成員可以在其定義(§17.4.4)或構造函數(§16.2.5)中初始化。例如:
template<typename T>
struct X {
int m1 = 7;
T m2;
X(const T& x) :m2{x} { }
};
X<int> xi {9};
X<string> xs {"Rapperswil"};
非 static 數據成員可以是const 的,但遺憾的是不能是 constexpr 的。
23.4.2? 成員函數(Member? Functions)
和“普通類”一樣,類模板的非 static 成員函數可以在類內或類外定義。例如:
template<typename T>
struct X {
void mf1() { /* ... */ } // 類內定義
void mf2();
};
template<typename T>
void X<T>::mf2() { /* ... */ } // 類外定義
類似地,模板的成員函數可以是虛擬的,也可以不是。但是,虛擬成員函數不能同時是成員函數模板(§23.4.6.2)。
23.4.3? 類成員另名(Member? Type Aliases)
??? 成員類型別名,無論是使用 using 還是 typedef(§6.5)引入,在類模板的設計中都發揮著重要作用。它們以一種易于從類外部訪問的方式定義類的相關類型。例如,我們將容器的迭代器和元素類型指定為別名:
template<typename T>
class Vector {
public:
using value_type = T;
using iterator = Vector_iter<T>; // Vector_iter 在它處定義
// ...
};
模板參數名稱 T 只能由模板本身訪問,因此為了讓其他代碼引用元素類型,我們必須提供別名。
??? 類型別名在泛型編程中發揮著重要作用,它允許類的設計者為不同類(和類模板)中的類型提供具有通用語義的通用名稱。作為成員別名的類型名稱通常稱為關聯類型。value_type 和iterator名稱借用自標準庫的容器設計(§33.1.3)。如果某個類缺少所需的成員別名,則可以使用特征(trait)進行補償(§28.2.4)。
23.4.4? static成員另名(static Members)
??? 未在類中定義的 static 數據或函數成員在程序中必須具有唯一定義。
例如:
template<typename T>
struct X {
static constexpr Point p {100,250}; // Point 必面為一個文字量類型(§10.4.3)
static const int m1 = 7;
static int m2 = 8; // 錯: 非 const
static int m3;
static void f1() { /* ... */ }
static void f2();
};
template<typename T> int X<T>::m1 = 88; // 錯: 兩個初始化器
template<typename T> int X<T>::m3 = 99;
template<typename T> void X::<T>::f2() { /* ... */ }
對于非模板類,文字量類型的 const 或 conexpr 靜態數據成員可以在類內初始化,無需在類外定義(§17.4.5,§iso.9.2)。
??? 靜態成員僅在使用時才需要定義(§iso.3.2,§iso.9.4.2,§16.2.12)。例如:
template<typename T>
struct X {
static int a;
static int b;
};
int? p = &X<int>::a;
??? 如果程序中僅提及 X<int>,則我們會得到 X<int>::a 的“未定義”錯誤,但不會得到 X<int>::b 的“未定義”錯誤。
23.4.5? 成員類型(Member Types)
??? 對于“普通類”,我們可以將類型定義為成員。通常,這種類型可以是類或枚舉。例如:
template<typename T>
struct X {
enum E1 { a, b };
enum E2; // 錯 : 未知底層類型
enum class E3;
enum E4 : char;
struct C1 { /* ... */ };
struct C2;
};
template<typename T>
enum class X<T>::E3 { a, b }; // 需要
template<typename T>
enum class X<T>::E4 : char { x, y }; // 需要
template<typename T>
struct X<T>::C2 { /* ... */ }; // 需要
成員枚舉的類外定義僅允許用于我們知道其底層類型的枚舉(§8.4)。
??? 與往常一樣,非 class enum 的枚舉器放在枚舉的作用域內;也就是說,對于成員枚舉,枚舉器位于其類的作用域內。
23.4.6? 成員模板(Member Template)
??? 類或類模板可以具有本身就是模板的成員。這使我們能夠以令人滿意的控制度和靈活性來表示相關類型。例如,復數最好表示為某些標量類型的值對:
template<typename Scalar>
class complex {
Scalar re, im;
public:
complex() :re{}, im{} {} // 默認構造函數
template<typename T>
complex(T rr, T ii =0) :re{rr}, im{ii} { }
complex(const complex&) = default; // 復制構造函數
template<typename T>
complex(const complex<T>& c) : re{c.real()}, im{c.imag()} { }
// ...
};
這允許復數類型之間進行數學上有意義的轉換,同時禁止不良的窄化轉換(§10.5.2.6):
complex<float> cf; // 默認值
complex<double> cd {cf}; // OK: float 轉換為 double
complex<float> cf2 {cd}; // 錯: 無隱式的 double->float 轉換
complex<float> cf3 {2.0,3.0}; // 錯: 無隱式的 double->float 轉換
complex<double> cd2 {2.0F,3.0F}; // OK: float 轉換為 double
class Quad {
// 無到 int 的轉換
};
complex<Quad> cq;
complex<int> ci {cq}; // 錯: 無 Quad 到 int 的轉換
??? 給定此 complex 定義,我們可以從 complex<T2> 構造 complex<T1>,或者從一對 T2 值構造 complex<T1>,當且僅當我們可以從 T2 構造 T1 時。這似乎是合理的。
??? 請注意,complex<double> 到 complex<float> 情況下的窄化錯誤直到 complex<float> 的模板構造函數實例化時才會被捕獲,而且僅僅是因為我在構造函數的成員初始化器中使用了 {} 初始化語法 (§6.3.5)。該語法不允許窄化。
??? 使用(舊)() 語法會讓我們面臨窄化錯誤。例如:
template<typename Scalar>
class complex { // 舊風格
Scalar re, im;
public:
complex() :re(0), im(0) { }
template<typename T>
complex(T rr, T ii =0) :re(rr), im(ii) { }
complex(const complex&) = default; // 復制構造函數
template<typename T>
complex(const complex<T>& c) : re(c.real()), im(c.imag()) { }
// ...
};
complex<float> cf4 {2.1,2.9}; // ouch! 窄化
complex<float> cf5 {cd}; // ouch! 窄化
我認為這是在使用 {} 符號進行初始化時保持一致的另一個原因。
23.4.6.1? 模板和構造函數(Templates and Constructors)
??? 為了盡量減少混淆的可能性,我明確添加了一個默認的復制構造函數。省略它不會改變定義的含義:complex 仍將獲得默認的復制構造函數。由于技術原因,模板構造函數永遠不會用于生成復制構造函數,因此如果沒有明確聲明的復制構造函數,則會生成默認的復制構造函數。同樣,復制賦值、移動構造函數和移動賦值(§17.5.1,§17.6,§19.3.1)必須定義為非模板運算符,否則將生成默認版本。
23.4.6.2? 模板和virtual(Templates and virtual)
??? 成員模板不能是virtual的。例如:
class Shape {
// ...
template<typename T>
virtual bool intersect(const T&) const =0; // 錯 : virtual 模板
};
這肯定是無效的。如果允許,則不能使用傳統的用于實現 virtual 函數的虛函數表技術(§3.2.3)。每次有人使用新參數類型調用 intersect() 時,鏈接器都必須向 Shape 類的虛擬表添加一個新條目。以這種方式使鏈接器的實現復雜化被認為是不可接受的。特別是,處理動態鏈接需要與最常用的實現技術截然不同的實現技術。
23.4.6.3 ?使用嵌套(Use of Nesting)
??? 一般來說,盡可能將信息局部化。這樣,名稱更容易找到,也不太可能干擾程序中的其他內容。這種思路導致將類型定義為成員。這樣做通常是一個好主意。但對于類模板的成員,我們必須考慮參數化是否適合成員類型。形式上,模板的成員依賴于模板的所有參數。如果成員的行為實際上沒有使用每個模板參數,這可能會產生不良的副作用。一個著名的例子是鏈接列表的鏈接類型。考慮:
template<typename T, typename Allocator>
class List {
private:
struct Link {
T val;
Link? succ;
Link? prev;
};
// ...
};
這里,Link 是 List 的一個實現細節。因此,它似乎是在 List 的作用域內定義并保持私有的最佳類型的完美示例。這是一種流行的設計,通常效果很好。但令人驚訝的是,與使用非本地 Link 類型相比,它可能意味著性能成本。假設 Link 的任何成員都不依賴于 Allocator 參數,并且我們需要 List<double ,My_allocator> 和 List<double ,Your_allocator>。現在 List<double ,My_allocator>::Link 和 List<double ,Your_allocator>::Link 是不同的類型,因此使用它們的代碼(沒有巧妙的優化器)不可能相同。也就是說,當 Link 僅使用 List 的兩個模板參數之一時,將其作為成員意味著一些代碼膨脹。這導致我們考慮一種 Link 不是成員的設計:
template<typename T, typename Allocator>
class List;
template<typename T>
class Link {
template<typename U, typename A>
friend class List;
T val;
Link? succ;
Link? prev;
};
template<typename T, typename Allocator>
class List {
// ...
};
我將 Link 的所有成員設為私有,并授予 List 訪問權限。除了將名稱 Link 設為非本地之外,這保留了 Link 是 List 的實現細節的設計意圖。
??? 但是,如果嵌套類不被視為實現細節怎么辦?也就是說,如果我們需要一種適用于各種用戶的關聯類型怎么辦?考慮一下:
template<typename T, typename A>
class List {
public:
class Iterator {
Link<T>? current_position;
public:
// ... 常規迭代器運算...
};
Iterator<T,A> begin();
Iterator<T,A> end();
// ...
};
這里,成員類型 List<T,A>::Iterator(顯然)不使用第二個模板參數 A。但是,由于 Iterator 是一個成員,因此形式上依賴于 A(編譯器不知道任何相反的情況),我們不能編寫一個函數來處理List,而不管它們是如何使用分配器構造的:
void fct(List<int>::Iterator b, List<int>::Iterator e) // 錯 : List取 2 個參數
{
auto p = find(b,e,17);
// ...
}
void user(List<int,My_allocator>& lm, List<int,Your_allocator>& ly)
{
fct(lm.begin(),lm.end());
fct(ly.begin(),ly.end());
}
相反,我們需要編寫一個依賴于分配器參數的函數模板:
void fct(List<int,My_allocator>::Iterator b, List<int,My_allocator>::Iterator e)
{
auto p = find(b,e,17);
// ...
}
但是,這會破壞我們的 user():
void user(List<int,My_allocator>& lm, List<int,Your_allocator>& ly)
{
fct(lm.begin(),lm.end());
fct(ly.begin(),ly.end()); // 錯 : fct 取 List<int,My_allocator>::Iterators
}
我們可以制作一個模板并為每個分配器生成單獨的特化。但是,這會為每次使用 Iterator 生成一個新的特化,因此這可能會導致嚴重的代碼膨脹 [Tsafrir,2009]。同樣,我們通過將 Link 移出類模板來解決問題:
template<typename T>
struct Iterator {
Link<T>? current_position;
};
template<typename T, typename A>
class List {
public:
Iterator<T> begin();
Iterator<T> end();
// ...
};
這使得每個具有相同第一個模板參數的 List 的迭代器在類型方面都可以互換。在這種情況下,這正是我們想要的。我們的 user() 現在按定義工作。如果 fct() 被定義為函數模板,那么 fct() 的定義將只有一個副本(實例化)。我的經驗法則是“在模板中的避免嵌套類型,除非它們真正依賴于每個模板參數。”這是一般規則的一個特例,以避免代碼中不必要的依賴關系。
23.4.7? 友元(Friends)
??? 如 §23.4.6.3 所示,模板類可以將函數指定為友函數。考慮 §19.4 中的 Matrix 和 Vector 示例。通常,Matrix 和 Vector 都是模板:
template<typename T> class Matrix;
template<typename T>
class Vector {
T v[4];
public:
friend Vector operator?<>(const Matrix<T>&, const Vector&);
// ...
};
template<typename T>
class Matrix {
Vector<T> v[4];
public:
friend Vector<T> operator?<>(const Matrix&, const Vector<T>&);
// ...
};
友函數名稱后面需要有 <>,以明確表明該友函數是模板函數。如果沒有 <>,則會假定為非模板函數。然后可以定義乘法運算符來直接從 Vector 和 Matrix 訪問數據:
template<typename T>
Vector<T> operator?(const Matrix<T>& m, const Vector<T>& v)
{
Vector<T> r;
// ... 使用 m.v[i] 和 v.v[i] 直接訪問元素 ...
return r;
}
友元不會影響模板類的定義作用域,也不會影響模板的使用作用域。相反,友函數和運算符是通過基于其參數類型的查找來找到的(§14.2.4,§18.2.5,§iso.11.3)。與成員函數一樣,友函數只有在使用時才會實例化(§26.2.1)。
??? 與其他類一樣,類模板可以將其他類指定為友元。例如:
class C;
using C2 = C;
template<typename T>
class My_class {
friend C; // OK: C 是一個類
friend C2; // OK: C2 是類 C 的別名
friend C3; // 錯:作用域內無類 C3
friend class C4; // OK: 引入一個新的類 C4
};
當然,有趣的情況是友元依賴于模板參數的情況。例如:
template<typename T>
class my_other_class {
friend T; //我的參數是我的友元!
friend My_class<T>; // 具有相應參數的 My_class 是我的友元
friend class T; // 錯: 重復的“類”
};
與以往一樣,友元既不可繼承也不可傳遞(§19.4)。例如,盡管 My_class<int> 是友元,并且 C 是 My_class<int> 的友元,但 C 并未成為 My_other_class<int> 的友元。
??? 我們不能直接將模板設為類的友元,但我們可以將友元聲明設為模板。例如:
template<typename T, typename A>
class List;
template<typename T>
class Link {
template<typename U, typename A>
friend class List;
// ...
};
??? 遺憾的是,沒有辦法說 Link<X> 應該只是 List<X> 的友元。
友元類的設計目的是允許表示緊密相關概念的小集群。復雜的友元關系模式幾乎肯定是一個設計錯誤。
23.5? 函數模板 (Function Templates ?)
??? 對于許多人來說,模板的第一個也是最明顯的用途是定義和使用容器類,例如 vector (§31.4),list (§31.4.2) 和 map (§31.4.3)。不久之后,就需要使用函數模板來操作此類容器。對 vector 進行排序就是一個簡單的例子:
template<typename T> void sort(vector<T>&); // declaration
void f(vector<int>& vi, vector<string>& vs)
{
sort(vi); // sort(vector<int>&);
sort(vs); // sort(vector<string>&);
}
調用函數模板時,函數參數的類型決定使用哪個版本的模板;也就是說,模板參數是從函數參數推導出來的(§23.5.2)。
??? 當然,函數模板必須在某處定義(§23.7):
template<typename T>
void sort(vector<T>& v) // definition
// Shell sort (Knuth, Vol. 3, pg. 84)
{
const size_t n = v.siz e();
for (int gap=n/2; 0<gap; gap/=2)
for (int i=gap; i<n; i++)
for (int j=i?gap; 0<=j; j?=gap)
if (v[j+gap]<v[j]) { // swap v[j] and v[j+gap]
T temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
請將此定義與 §12.5 中定義的 sort() 進行比較。此模板化版本更簡潔、更短,因為它可以依賴有關其排序元素類型的更多信息。通常,它也更快,因為它不依賴于指向函數的指針來進行比較。這意味著不需要間接函數調用,并且內聯簡單的 < 很容易。
??? 進一步簡化是使用標準庫模板 swap() (§35.5.2)將操作簡化為其自然形式:
if (v[j+gap]<v[j])
swap(v[j],v[j+gap]);
這不會引入任何新的開銷。更好的是,標準庫 swap() 使用移動語義,因此我們可能會看到加速(§35.5.2)。
在此示例中,運算符 < 用于比較。但是,并非每個類型都有 < 運算符。這限制了此版本 sort() 的使用,但通過添加參數可以輕松避免此限制(參見 §25.2.3)。例如:
template<typename T, typename Compare = std::less<T>>
void sort(vector<T>& v) // definition
// Shell sort (Knuth, Vol. 3, pg. 84)
{
Compare cmp; // 創建一個 Compare 對象
const size_t n = v.siz e();
for (int gap=n/2; 0<gap; gap/=2)
for (int i=gap; i<n; i++)
for (int j=i?gap; 0<=j; j?=gap)
if (cmp(v[j+gap],v[j]))
swap(v[j],v[j+gap]);
}
我們現在可以使用默認比較操作(<)進行排序或者提供我們自己的比較操作:
struct No_case {
bool operator()(const string& a, const string& b) const; // 忽略大小寫的比較
};
void f(vector<int>& vi, vector<string>& vs)
{
sort(vi); // sor t(vector<int>&)
sort<int,std::greater<int>>(vi); // sort(vector<int>&) 大于
sort(vs); // sort(vector<str ing>&)
sort<string,No_case>(vs); // sort(vector<str ing>&) 使用 No_case
}
遺憾的是,只能指定尾隨模板參數的規則導致我們在指定比較操作時必須指定(而不是推斷)元素類型。
??? 函數模板參數的明確指定在§23.5.2 中解釋。
23.5.1? 函數模板參數(Function Template Arguments)
??? 函數模板對于編寫可應用于各種容器類型的通用算法至關重要(§3.4.2,§32.2)。從函數參數推導出調用的模板參數的能力至關重要。
??? 只要函數參數列表唯一地標識模板參數集,編譯器就可以從調用中推斷出類型和非類型參數。例如:
template<typename T, int max>
struct Buffer {
T buf[max];
public:
// ...
};
template<typename T, int max>
T& lookup(Buffer<T,max>& b, const char? p);
Record& f(Buffer<string,128>& buf, const char? p)
{
return lookup(buf,p); // 使用 lookup(),其中 T 是 string 而 i 是 128
}
這里,lookup() 的 T 被推斷為 string,max 被推斷為 128。
??? 請注意,類模板參數永遠不會被推導。原因是,類的多個構造函數提供的靈活性使得這種推導在許多情況下無法進行,在更多情況下則難以理解。相反,特化(§25.3)提供了一種機制,用于在模板的替代定義之間進行隱式選擇。如果我們需要創建一個推導類型的對象,我們通常可以通過調用一個函數來進行推導(和創建)。例如,考慮標準庫的 make_pair() 的一個簡單變體(§34.2.4.1):
template<typename T1, typename T2>
pair<T1,T2> make_pair(T1 a, T2 b)
{
return {a,b};
}
auto x = make_pair(1,2); // x 是一個 pair<int,int>
auto y = make_pair(string("New York"),7.7); // y 是一個 pair<string,double>
如果模板參數不能從函數參數中推導出來(§23.5.2),我們必須明確指定它。這與為模板類明確指定模板參數的方式相同(§25.2,§25.3)。例如:
template<typename T>
T? create(); //創建一個 T 并返回一個指向它的指針
void f()
{
vector<int> v; // 類, 模板參數 int
int? p = create<int>(); // 函數, 模板參數 int
int? q = create(); // 錯: 不能推導模板參數
}
使用顯式指定來為函數模板提供返回類型的做法非常常見。它允許我們定義對象創建函數系列(例如 create())和轉換函數系列(例如 §27.2.2)。static_cast、dynamic_cast 等的語法(§11.5.2,§22.2.1)與顯式限定的函數模板語法相匹配。
??? 在某些情況下,可以使用默認模板參數來簡化顯式限定(§25.2.5.1)。
23.5.2? 函數模板參數推導(Function Template Arguments Deduction)
編譯器可以從具有以下結構組成的類型的模板函數參數推導出類型模板參數 T 或 TT,以及非類型模板參數 I(§iso.14.8.2.1):
這里,args_TI 是一個參數列表,可以通過遞歸應用這些規則來確定 T 或 I,而 args 是一個不允許推導的參數列表。如果不能以這種方式推導所有參數,則調用會產生歧義。例如:
template<typename T, typename U>
void f(const T?, U(?)(U));
int g(int);
void h(const char? p)
{
f(p,g); // T 是 char, U 是 int
f(p,h); // 錯: 不能推導 U
}
查看 f() 第一次調用的參數,我們很容易推斷出模板參數。查看 f() 的第二次調用,我們發現 h() 與模式 U(?)(U) 不匹配,因為 h() 的參數和返回類型不同。
??? 如果模板參數可以從多個函數參數推導而來,則每次推導的結果必須是同一類型。否則,調用將出錯。例如:
template<typename T>
void f(T i, T? p);
void g(int i)
{
f(i,&i); //OK
f(i,"Remember!"); // 錯, 歧義: T 是 int 或 T 是 const char?
}
23.5.2.1? 參考推導(Reference Deduction)
對左值和右值采取不同的操作可能很實用。考慮一個用于保存 {整數,指針} 對的類:
template<typename T>
class Xref {
public:
Xref(int i, T? p) // 存儲一個推針: Xref 是 owner
:index{i}, elem{p}, owner{true}
{ }
Xref(int i, T& r) // 存儲一個指向 r 的指針, 由其它擁有
:index{i}, elem{&r}, owner{false}
{ }
Xref(int i, T&& r) // 將 r 移入 Xref, Xref 是 owner
:index{i}, elem{new T{move(r)}}, owner{true}
{ }
?Xref()
{
if(owned) delete elem;
}
// ...
private:
int index;
T? elem;
bool owned;
};
因此:
string x {"There and back again"};
Xref<string> r1 {7,"Here"}; // r1 擁有字符串 "Here" 的一個副本
Xref<string> r2 {9,x}; // r2 是對 x 和引用
Xref<string> r3 {3,new string{"There"}}; // r3 擁有字符串 "There"
這里,r1 選擇 Xref(int,string&&),因為 x 是右值。類似地,r2 選擇Xref(int,string&),因為 x 是左值。
左值和右值通過模板參數推導來區分:類型 X 的左值被推導為 X&,而右值則被推導為 X 。這不同于將值綁定到非模板參數右值引用(§12.2.1),但對于參數轉發(§35.5.1)尤其有用。考慮編寫一個工廠函數,在自由存儲中創建 Xref 并向它們返回 unique_ptr:
template<typename T>
T&& std::forward(typename remove_reference<T>::type& t) noexcept; // §35.5.1
template<typename T>
T&& std::forward(typename remove_reference<T>::type&& t) noexcept;
template<typename TT, typename A>
unique_ptr<TT> make_unique(int i, A&& a) // make_shared 的簡單變體 (§34.3.2)
{
return unique_ptr<TT>{new TT{i,forward<A>(a)}};
}
我們希望 make_unique<T>(arg) 從 arg 構造一個 T,而不會進行任何虛假復制。為此,必須保持左值/右值區別。考慮:
auto p1 = make_unique<Xref<string>>(7,"Here");
“Here” 是一個右值,因此調用 forward(string&&),傳遞一個右值,從而調用 Xref(int,string&&) 從包含“Here”的字符串移動。
??? 更有趣(微妙)的情況是:
auto p2 = make_unique<Xref<string>>(9,x);
??? 這里,x 是左值,因此調用 forward(string&),傳遞一個左值:forward() 的 T 被推導為 string&,因此返回值變為 string& &&,即 string&(§7.7.3)。因此,對左值 x 調用 Xref(int,string&),因此復制了 x 。
??? 遺憾的是,make_unique() 不是標準庫的一部分,但它仍然受到廣泛支持。使用可變參數模板進行轉發(§28.6.3),定義可以接受任意參數的 make_unique() 相對容易。
23.5.3? 函數模板重載(Function Template Overloading)
我們可以聲明多個同名的函數模板,甚至可以聲明同名的函數模板和普通函數的組合。當調用重載函數時,需要進行重載解析以找到要調用的正確函數或函數模板。例如:
template<typename T>
T sqrt(T);
template<typename T>
complex<T> sqrt(complex<T>);
double sqrt(double);
void f(complex<double> z)
{
sqrt(2); // sqrt<int>(int)
sqrt(2.0); // sqrt(double)
sqrt(z); // sqrt<double>(complex<double>)
}
函數模板是函數概念的泛化,同樣,在存在函數模板的情況下,解析規則是函數重載解析規則的泛化。基本上,對于每個模板,我們都會找到最適合函數參數集的特化。然后,我們將通常的函數重載解析規則應用于這些特化和所有普通函數(§iso.14.8.3):
[1] 查找將參與重載解析的函數模板特化集(§23.2.2)。通過考慮每個函數模板并確定如果范圍內沒有其他同名函數模板或函數,將使用哪些模板參數(如果有)。對于調用 sqrt(z),這會產生 sqrt<double>(complex<double>) 和 sqrt<complex<double>>(complex<double>) 候選。另請參閱 §23.5.3.2。
[2] 如果可以調用兩個函數模板,并且其中一個比另一個更專業(§25.3.3),則在以下步驟中僅考慮最專業的模板函數。對于 sqrt(z) 調用,這意味著sqrt<double>(complex<double>) 優于sqrt<complex<double>>(complex<double>):任何與 sqrt<T>(complex<T>) 匹配的調用也匹配 sqrt<T>(T)。
[3] 像對待普通函數一樣,對這組函數以及任何普通函數進行重載解析 (§12.3)。如果函數模板的參數已通過模板參數推導確定 (§23.5.2),則該參數不能同時應用提升、標準轉換或用戶定義轉換。對于 sqrt(2),sqrt<int>(int) 是精確匹配,因此它優于sqrt(double)。
[4] 如果函數和特化同樣匹配,則該函數是首選。因此,對于 sqrt(2.0),sqrt(double) 優于 sqrt<double>(double)。
[5] 如果未找到匹配項,則調用會出錯。如果最終得到兩個或更多同樣好的匹配項,則調用會產生歧義并出錯。
例如:
template<typename T>
T max(T,T);
const int s = 7;
void k()
{
max(1,2); // max<int>(1,2)
max('a','b'); // max<char>(’a’,’b’)
max(2.7,4.9); // max<double>(2.7,4.9)
max(s,7); // max<int>(int{s},7) (使用平凡轉換)
max('a',1); // 錯誤: 歧義: max<char,char>() 或 max<int,int>()?
max(2.7,4); // 錯誤: 歧義: max<double,double>() 或 max<int,int>()?
}
最后兩個調用的問題在于,直到模板參數被唯一確定之后,我們才應用提升和標準轉換。沒有規則告訴編譯器優先選擇一種解決方案而不是另一種解決方案。在大多數情況下,語言規則將微妙的決定權留給程序員可能是件好事。令人驚訝的歧義錯誤的替代方案是意外解決方案帶來的令人驚訝的結果。人們對重載解析的“直覺”差異很大,因此不可能設計一套完全直觀的重載解析規則。
23.5.3.1? 歧義消除(Ambiguity Resolution)
??? 我們可以通過明確的限定來解決這兩個歧義:
void f()
{
max<int>('a',1); // max<int>(int(’a’),1)
max<double>(2.7,4); // max<double>(2.7,double(4))
}
或者,我們可以添加適當的聲明:
inline int max(int i, int j) { return max<int>(i,j); }
inline double max(int i, double d) { return max<double>(i,d); }
inline double max(double d, int i) { return max<double>(d,i); }
inline double max(double d1, double d2) { return max<double>(d1,d2); }
void g()
{
max('a',1); // max(int(’a’),1)
max(2.7,4); // max(2.7,4)
}
對于普通函數,適用普通重載規則(§12.3),并且使用內聯可確保不會產生額外的開銷。
??? max() 的定義很簡單,所以我們可以直接實現比較,而不是調用 max() 的特化。但是,使用模板的顯式特化是定義此類解析函數的一種簡單方法,并且可以通過避免在多個函數中使用幾乎相同的代碼來幫助維護。
23.5.3.2? 參數替換失敗(Argument Substitution Failure)
??? 在為函數模板尋找一組參數的最佳匹配時,編譯器會考慮該參數是否可以按照完整函數模板聲明(包括返回類型)所要求的方式使用。例如:
template<typename Iter>
typename Iter::value_type mean(Iter first, Iter last);
void f(vector<int>& v, int? p, int n)
{
auto x = mean(v.begin(),v.end()); // OK
auto y = mean(p,p+n); // error
}
這里,x 的初始化成功,因為參數匹配,并且 vector<int>::iterator 有一個名為 value_type 的成員。y 的初始化失敗,因為即使參數匹配,int? 也沒有一個名為 value_type 的成員,所以我們不能說:
int?::value_type mean(int?,int?); //int* 沒有一個稱為 value_type 的成員
但是,如果 mean() 有另一種定義會怎樣?
template<typename Iter>
typename Iter::value_type mean(Iter first, Iter last); // #1
template<typename T>
T mean(T?,T?); //#2
void f(vector<int>& v, int? p, int n)
{
auto x = mean(v.begin(),v.end()); // OK: call #1
auto y = mean(p,p+n); // OK: call #2
}
這有效:兩個初始化都成功了。但是為什么我們在嘗試將 mean(p,p+n) 與第一個模板定義匹配時沒有收到錯誤?參數完全匹配,但通過替換實際模板參數(int?),我們得到了函數聲明:
int?::value_type mean(int?,int?); // int* 沒有稱為 value_type 的成員
當然,這是垃圾:指針沒有成員 value_type。幸運的是,考慮這種可能的聲明本身并不是錯誤。有一條語言規則 (§iso.14.8.2) 規定這種替換失敗不是錯誤。它只會導致模板被忽略;也就是說,模板不會為重載集貢獻特化。完成后,mean(p,p+n) 匹配被調用的聲明 #2。
??? 如果沒有“替換錯誤不代表失敗”規則,即使有無錯誤的替代方案(例如 #2),我們也會遇到編譯時錯誤。此外,此規則還為我們提供了一個在模板中進行選擇的通用工具。基于此規則的技術在 §28.4 中進行了描述。特別是,標準庫提供了 enable_if 來簡化模板的條件定義(§35.4.2)。
??? 該規則以無法發音的首字母縮略詞 SFINAE(Substitution Failure Is Not An Error)而聞名。SFINAE 通常用作動詞,其中“F”發音為“v”:“我 SFINAE 消除了那個構造函數。”這聽起來相當令人印象深刻,但我傾向于避免使用這種行話。“構造函數因替換失敗而被消除”對大多數人來說更清楚,并且對英語的破壞更小。
??? 因此,如果在生成候選函數以解析函數調用的過程中,編譯器發現自己生成的模板特化毫無意義,則該候選不會進入重載集合。如果模板特化會導致類型錯誤,則認為它是無意義的。在此,我們僅考慮聲明;除非實際使用,否則不會考慮(或生成)模板函數定義和類成員的定義。例如:
template<typename Iter>
Iter mean(Iter first, Iter last) // #1
{
typename Iter::value_type = ?first;
// ...
}
template<typename T>
T? mean(T?,T?); //#2
void f(vector<int>& v, int? p, int n)
{
auto x = mean(v.begin(),v.end()); // OK: call #1
auto y = mean(p,p+n); // 錯誤 : 歧義
}
對于 mean(p,p+n),mean() #1 的聲明沒有問題。由于類型錯誤,編譯器不會啟動實例化該 mean() 的主體并將其消除。
??? 此處的結果是一個歧義錯誤。如果沒有 mean() #2,則將選擇聲明 #1,并且我們將遭遇實例化時錯誤。因此,一個函數可能被選為最佳匹配,但仍然無法編譯。
23.5.3.3? 重載和派生(Overloading and Derivation)
重載解析規則確保函數模板與繼承正確交互:
template<typename T>
class B { /* ... */ };
template<typename T>
class D : public B<T> { /* ... */ };
template<typename T> void f(B<T>?);
void g(B<int>? pb, D<int>? pd)
{
f(pb); // f<int>(pb) of course
f(pd); // f<int>(static_cast<B<int>*>(pd));
// 使用 D<int>* 到 B<int>* 的標準轉換
}
在這個例子中,函數模板 f() 接受任意類型 T 的 B<T>?。我們有一個類型為 D<int>? 的參數,因此編譯器很容易推斷,通過選擇 T 為 int,該調用可以唯一地解析為 f(B<int>?) 的調用。
23.5.3.4? 重載和非推導參數(Overloading and Non-Deduced Parameters)
??? 不參與模板參數推導的函數參數將被視為非模板函數的參數。特別是,通常的轉換規則適用。考慮:
template<typename T, typename C>
T get_nth(C& p, int n); // 取第n個元素
此函數大概返回類型 C 的容器的第 n 個元素的值。由于 C 必須從調用中的 get_nth() 的實際參數推導而來,因此轉換不適用于第一個參數。但是,第二個參數完全是普通的,因此會考慮所有可能的轉換。例如:
struct Index {
operator int();
// ...
};
void f(vector<int>& v, short s, Index i)
{
int i1 = get_nth<int>(v,2); // 準確匹配
int i2 = get_nth<int>(v,s); // 標準轉換: short 轉 int
int i3 = get_nth<int>(v,i); // 用戶義定轉: Index 轉 int
}
這種符號有時被稱為顯式特化(§23.5.1)。
23.6? 模板別名 (Template Aliases )
??? 我們可以使用 using 語法或 typedef 語法(§6.5)為類型定義別名。using 語法更通用,因為它可用于為模板定義別名,并綁定其部分參數。考慮:
template<typename T, typename Allocator = allocator<T>> vector;
using Cvec = vector<char>; // 兩個參數都是有約束的
Cvec vc = {'a', 'b', 'c'}; // vc 是一個 vector<char,allocator<char>>
template<typename T>
using Vec = vector<T,My_alloc<T>>; //使用我的 allocator的vector(第二個參數有約束)
Vec<int> fib = {0, 1, 1, 2, 3, 5, 8, 13}; // fib 是一個vector<int,My_alloc<int>>
??? 一般來說,如果我們綁定模板的所有參數,我們會得到一個類型,但如果我們只綁定一些參數,我們會得到一個模板。請注意,我們在別名定義中使用時得到的始終是一個別名。也就是說,當我們使用別名時,它完全等同于對原始模板的使用。例如:
vector<char,alloc<char>> vc2 = vc; // vc2 和 vc 類型相同
vector<int,My_alloc<int>> verbose = fib; // verbose 和 fib 類型相同
別名和原始模板的等價性意味著,如果您特化模板,則在使用別名時您會(正確地)獲得特化。例如:
template<int>
struct int_exact_traits { // 思想: int_exact_traits<N>::type 是恰好有N位的類型
using type = int;
};
template<>
struct int_exact_traits<8> {
using type = char;
};
template<>
struct int_exact_traits<16> {
using type = short;
};
template<int N>
using int_exact = typename int_exact_traits<N>::type; //為方便記法而定義類型
int_exact<8> a = 7; // int_exact<8> 是一個具有8位的int
??? 如果特化沒有通過別名使用,我們就不能聲稱 int_exact 只是int_exact_traits<N>::type 的別名;它們的行為會有所不同。另一方面,你無法定義別名的特化。如果你能夠這樣做,人類讀者很容易對特化的內容感到困惑,因此沒有提供用于特化別名的語法。
23.7? 源碼組織 (Source Code Organization)
??? 使用模板組織代碼有三種相當明顯的方法:
[1] 在編譯單元中使用模板之前,包含模板定義。
[2] 在編譯單元中使用模板之前,僅包含模板聲明。在編譯單元的稍后部分(可能在使用模板之后)包含模板定義。
[3] 在編譯單元中使用模板之前,僅包含模板聲明。在其他編譯單元中定義模板。
由于技術和歷史原因,不提供選項 [3],即單獨編譯模板定義及其使用。迄今為止最常見的方法是將您使用的模板的定義包含(通常是 #include)到您使用它們的每個編譯單元中,并依靠你的實現來優化編譯時間并消除目標代碼重復。例如,我可能會在頭文件 out.h 中提供一個模板 out():
// file out.h:
#include<iostream>
template<typename T>
void out(const T& t)
{
std::cerr << t;
}
我們將在需要 out() 的地方 #include 此頭文件。例如:
// file user1.cpp:
#include "out.h"
// use out()
和
#include "out.h"
// use out()
也就是說,out() 的定義及其所依賴的所有聲明都 #included 在幾個不同的編譯單元中。編譯器負責在需要時(僅)生成代碼并優化讀取冗余定義的過程。此策略將模板函數視為與內聯函數相同的方式。
??? 這種策略的一個明顯問題是,用戶可能會意外地依賴僅為 out() 定義而包含的聲明。可以通過采取方法 [2]“稍后包含模板定義”、使用命名空間、避免使用宏以及通常減少包含的信息量來限制這種危險。理想的做法是盡量減少模板定義對其環境的依賴。
??? 為了在我們的簡單 out() 示例中使用“稍后包含模板定義”方法,我們首先將 out.h 拆分為兩個。聲明放入 .h 文件中:
// file outdecl.h:
template<typename T>
void out(const T& t);
定義放入 out.cpp:
// file out.cpp:
#include<iostream>
template<typename T>
void out(const T& t)
{
std::cerr << t;
}
用戶現在同時 #inclue 兩者:
// file user3.cpp:
#include "out.h"
// use out()
#include "out.cpp"
??? 這最大限度地降低了模板實現對用戶代碼產生不良影響的可能性。遺憾的是,這也增加了用戶代碼中的某些內容(例如宏)對模板定義產生不良影響的可能性。
??? 與以往一樣,非 inline、非模板函數和 static 成員(§16.2.12)必須在某些編譯單元中具有唯一定義。這意味著最好不要將這些成員用于許多編譯單元中包含的模板。如 out() 所示,模板函數的定義可能會在不同的編譯單元中復制,因此請注意可能會巧妙地改變定義含義的上下文:
// file user1.cpp:
#include "out.h"
// use out()
和
// file user4.cpp:
#define std MyLib
#include "out.c"
// use out()
這種偷偷摸摸且容易出錯的宏使用方式會更改 out 的定義,導致 user4.cpp 的定義與 user1.cpp 的定義不同。這是一個錯誤,但實現可能無法發現這個錯誤。這種錯誤在大型程序中很難檢測到,因此請小心,盡量減少模板的上下文依賴性,并對宏保持高度警惕(§12.6)。
??? 如果你需要對實例化上下文有更多的控制,則可以使用顯式實例化和 extern template(§26.2.2)。
23.7.1? 鏈接(Linkage)
??? 模板的鏈接規則是生成的類和函數的鏈接規則(§15.2、§15.2.3)。這意味著,如果類模板的布局或內聯函數模板的定義發生變化,則必須重新編譯使用該類或函數的所有代碼。
??? 對于在頭文件中定義并“隨處”包含的模板,這可能意味著大量的重新編譯,因為模板往往在頭文件中包含大量信息,比使用 .cpp 文件的非模板代碼更多。特別是,如果使用動態鏈接庫,必須注意確保模板的所有用途都得到一致定義。
??? 有時,通過將復雜模板庫的使用封裝在具有非模板接口的函數中,可以最大限度地減少對復雜模板庫的更改。例如,我可能想使用支持多種類型的通用數值庫來實現一些計算(例如,第 29 章,§40.4,§40.5,§40.6)。但是,我經常知道用于計算的類型。例如,在程序中,我可能始終使用 double 和 vector<double>。在這種情況下,我可以定義:
double accum(const vector<double>& v)
{
return accumulate(v.begin(),v.end(),0.0);
}
鑒于此,我可以在我的代碼中使用 accum() 的簡單非模板聲明:
double accum(const vector<double>& v);
對 std::accumulate 的依賴已消失在 .cpp 文件中,而我的其他代碼看不到該文件。此外,我只在該 .cpp 文件中遭受 #include<numeric> 的編譯時開銷。
??? 請注意,我借此機會簡化了 accum() 接口(與 std::accumulate() 相比)。通用性是優秀模板庫的一個關鍵屬性,但可視為特定應用程序的復雜性來源。
??? 我懷疑我不會將這種技術用于標準庫模板。這些模板多年來一直很穩定,并且為實現所熟知。特別是,我沒有費心嘗試封裝 vector<double>。然而,對于更復雜、深奧或經常更改的模板庫,這種封裝可能很實用。
23.8? 建議 (Advice)
[1] 使用模板來表達適用于多種參數類型的算法;§23.1。
[2] 使用模板來表達容器;§23.2。
[3] 請注意,template<class T> 和 template<typename T> 是同義詞;§23.2。
[4] 定義模板時,首先設計和調試非模板版本;然后通過添加參數進行概括;§23.2.1。
[5] 模板是類型安全的,但檢查發生得太晚;§23.3。
[6] 設計模板時,請仔細考慮其模板參數所假定的概念(要求);§23.3。
[7] 如果類模板應該是可復制的,請為其提供非模板復制構造函數和非模板復制賦值;§23.4.6.1。
[8] 如果類模板應該是可移動的,請為其提供非模板移動構造函數和非模板移動賦值;§23.4.6.1。
[9] 虛函數成員不能是模板成員函數;§23.4.6.2。
[10] 僅當類型依賴于類模板的所有參數時,才將其定義為模板的成員;
§23.4.6.3。
[11] 使用函數模板推斷類模板參數類型;§23.5.1。
[12] 重載函數模板以獲得各種參數類型的相同語義;§23.5.3。
[13] 使用參數替換失敗為程序提供合適的函數集;§23.5.3.2。
[14] 使用模板別名簡化符號并隱藏實現細節;§23.6。
[15] 沒有單獨的模板編譯:#include 模板定義在每個使用它們的編譯單元中;§23.7。
[16] 使用普通函數作為無法處理模板的代碼的接口;§23.7.1。
[17] 分別編譯大型模板和具有非平凡上下文依賴關系的模板;§23.7。
內容來源:
<<The?C++?Programming?Language >> 第4版,作者 Bjarne Stroustrup