模板與泛型是現代 C++ 程式中重要的內容,很多大型專案(如
QuantLib 或是 C++
的標準程式庫)大量使用模板與泛型相關技術。
即使開發時不常使用,但在閱讀一些重要程式庫時,仍舊無法避免必須理解模板與泛型的重要觀念。
OOP 並不直接處理『型態在程序執行之前都未知』的情况。而是『多態』允許在執行時根據物件的實際型態來決定使用哪個方法。實際上,所有類別的定義都必须在編譯時已知。
『泛型(Generics)』在編譯時就能知道型態。
C++ 中的模板(Templates)與泛型(Generics)是一種機制,允許開發人員編寫『不依賴於具體型態』的程式碼。
容器(如
vector)、迭代器等都是『泛型』(類別模板)概念的實現。
泛型為獨立於任何特定型態的編程技術。
『模板(Templates)』為 C++ 泛型的基礎。
『模板』就是創建一個『類別』或『函數』的藍圖或是公式。而這些藍圖或公式在未來將轉變為『具體』的類別或是函數。
模板支援將型態作為參數的程式設計方式,從而實現對泛型程式設計實作。
模板(Templates):C++ 模板是一種在『編譯期』進行『型態參數化』的方法,允許編寫通用的程式碼框架,可與不同的資料型態一起工作。而 C++ 中主要有兩種模板:
函數模板:定義通用的函數,該函數可使用不同的資料型態進行操作。
類別模板:定義通用的類別,可使用不同的資料型態來儲存和處理資料。
在 C++ 中,『泛型(Generics)』一般與『模板(Templates)』被視為同義詞。
泛型編程是一種編程風格,強調編寫出獨立於任何特定資料型態的程式碼。
例如,可使用函數模板來創建一個通用的排序函數,該函數可以用於整數、浮點數甚至自定義對象,只要這些物件支援比較操作即可:
template <typename T>
void sort(T arr[], int size) {
// 排序演算法實現
}
T
是一個『型態』佔位符,可在呼叫函數時適用於任何有效的 C++
型態來做替換。該方法提高了程式碼的靈活性和可重用性。當編譯器處理『模板定義』時,並不產生程式碼。只有當實例化出模板的一個特定版本時,編譯器才會產生程式碼。
故當『使用』模板時(非定義時)模板時,編譯器才會產生程式碼。
一般情況,當呼叫函數時,編譯器只需知道函數的宣告。同理,當使用類別型態的物件時,類別定義必須可知,則成員函數『定義』則不需已知,故可將『類別定義』與『函數宣告』放至『標頭檔
.h』中,而普通函數與類別的成員函數定義則放在『原始檔
.cpp』內。
模板則不同:為了實例化一個模板版本,編譯器需要掌握『函數模板』或『類別模板』成員函數的定義。故模板的『標頭檔
.h』一般會包含『宣告』與『定義』。
模板編譯錯誤:
第一階段:編譯模板。在此階段,編譯器通常不會發現很多錯誤。編譯器可以檢查語法錯誤,例如忘記分號或是變數名稱拼錯等。
第二階段:編譯器遇到模板使用時。在此階段,編譯器仍然没有很多可檢查的。
對於函数模板呼叫,編譯器一般會檢查時參數個數是否正確。還能檢查參數型態是否匹配。
對於類別模板,編譯器則可檢查使用者是否提供了正確數量的模板實參。
第三階段:模板實例化時。只有這個階段才能發現型態相關的錯誤。依赖於編譯器如何管理實例化,此類錯誤可能在連結期時才會報出錯誤。
函數模板可視為一個公式,可用來產生針對特定型態的函數版本(實例化)。
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}概念
函數模板是一種定義通用函數的方式,使得函數可以操作多種型態的資料,而不必為每種資料型態重寫程式碼。在定義函數模板時,不是使用具體的資料型態(如
int, float
等),而是使用『型態參數(Type Parameters)』。
函數模板的定義以 template
關鍵字開始,緊接著 <>,而 <>
後面跟著『模板參數列表(template parameter list)』。
模板定義中,模板參數列表不能為空。
『模板參數列表』內表示在函數定義中使用到的『型態』或『值』,與函數參數列表類似。
使用模板時有時需指定『模板實參』,指定時也必須使用
<> 將模板實參包含進去。
但有時又不需要指定模板實參,系統可根據一些線索推論出型態。
當系統推論不出型態時,則會出現編譯錯誤。
型態參數前必須使用關鍵字 class 或
typename(含義相同,可以互換使用):
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
T
是一個模板參數,代表一種未指定的資料型態。
當這個模板函數被呼叫時,編譯器會根據傳入參數的『實際型態』自動『實例化』一個具體版本的函數。
使用函數模板(實例化(instantiate)函數模板)
模板函數與一般函數一樣。當呼叫模板函數時,編譯器會根據呼叫該模板函數時提供的實參來推論模板參數列表內的型參型態,故通常不需要明確指定型態參數,編譯器會自動推斷出來。
有時候,系統無法根據所提供的實參推論出模板參數,此時就需要使用
<> 主動提供模板參數。
因此,在呼叫模板函數時,可先不看函數模板定義中 template<> 內有多少模板參數;主要是看函數模板內的函數名稱之後的參數數量。
編譯器實例化一個模板時,其使用實際的模板實參代替對應的模板參數來建立一個新的『實例』。
int a = 5, b = 10;
swap(a, b); // 編譯器推斷出 T 為 int
double x = 5.5, y = 10.5;
swap(x, y); // 編譯器推斷出 T 為 double『非型態模板參數』
除了定義『型態參數』,還可以在模板中定義『非型態參數(nontype parameter)』。
『非型態參數(nontype
parameter)』表示為一個『值』,而非型態。
透過特定的型態名稱,而『非』關鍵字
class 或 typename 來指定『非型態參數(nontype
parameter)』。
當模板被實例化時,『非型態參數(nontype
parameter)』可被『使用者提供』或由『編譯器推論』出的值所取代。
『非型態參數(nontype
parameter)』必須為『常數表達式』,使得編譯器可在編譯期實例化模板。
template<int a, int b>
int funcaddv2()
{
int addhe = a + b;
return addhe;
}
int result = funcaddv2<12, 13>(); // 透過 <> 來傳遞參數。『顯式』指定模板參數。
int a = 12;
int result2 = funcaddv2<a, 13>(); // 錯誤:『非型態模板參數』必須是『常數表達式』。
此例無『型態模板參數』,只有『非型態模板參數』。
透過 <>
來傳遞參數。此為『顯式』指定模板參數。
當系統推論不出型態時,則需『顯式』指定模板參數。
template<typename T, int a, int b>
int funcaddv3(T c)
{
int addhe = (int)c + a + b;
return addhe;
}
int result = funcaddv3<int, 11, 12>(13);
std::cout << result << std::endl; // 36
int result2 = funcaddv3<double, 11, 12>(13);
std::cout << result << std::endl; // 36,此時,系統會以用 <> 傳遞進去的型態為準,而不是以13推論出來的型態為準。
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
compare("hi", "mom");
編譯器會使用常數字面值的大小來替代 N 與
M,從而實例化函數模板:
int compare(const char (&p1)[3], const char (&p2)[4]);『非型態參數(nontype parameter)』可以是一個整數型態,或是一個指向物件或函數型態的『指標』或『左值參考』。
綁定到指標或參考非型態參數的實參必須具有『靜態』的生存期。
無法使用一個普通(非
static)局部變數,或動態物件作為指標或參考『非型態參數(nontype
parameter)』的實參。
inline 與 constexpr
的函數模板
函數模板可以宣告為 inline 或
constexpr。
// 正確
template <typename T> inline T min(const T&, const T&);
// 錯誤
// inline template <typename T> T min(const T&, const T&);函數模板的『特化』
有時候,可能需要對『特定型態』提供一個特殊的實現,這就是所謂的『模板特化』。
『模板特化』就是告訴編譯器,當特定的型態被用作模板參數時,使用一個特定的函數實現。
template <>
void swap<double>(double& a, double& b) {
// 特殊處理 double 型態的 swap
}
注意事項:
型態推斷:在大多數情況下,編譯器可以自動推斷模板參數的型態。但有時你可能需要手動指定型態,尤其是當函數無法從上下文中推斷出正確的型態時。
過度泛化:雖然函數模板可以提高程式碼的重用性,但過度泛化可能會導致程式碼難以理解和維護。因此,合理地使用模板對於寫出高質量的程式碼非常重要。
編譯錯誤:由於模板函數是在編譯時實例化的,所以任何與型態相關的錯誤都會在編譯時出現。這種錯誤有時可能難以理解,特別是在複雜的模板實例化中。
程式碼膨脹:每當使用新的型態實例化模板時,編譯器都會生成一份新的函數實現。如果濫用模板,可能導致可執行文件的大小顯著增加。
類別模板的主要目的是為了程式碼重用和型態安全。
編譯器無法為類別模板推論模板參數,故使用類別模板時,必須在模板名稱後面使用
<>
提供額外資訊(模板參數列表內的參數)。
定義類別模板:
類別模板的定義以 template
關鍵字開頭,後跟模板參數列表。例如:
template <typename T>
class Box {
public:
T contents;
void set(T value) {
contents = value;
}
T get() {
return contents;
}
};
T
是一個模板參數,代表了一種未指定的資料型態。當你實例化這個類別模板時,可用實際的資料型態(如
int, double,
string 等)來替換
T。使用類別模板:
實例化類別模板時,編譯器就會產生一個具體的類別。
每種型態都會產生一個不同的類別。
Box<int> intBox;
intBox.set(123);
Box<string> stringBox;
stringBox.set("Hello World");類別模板的成員函數
類別模板的成員函數定義可以寫在類別模板定義的標頭檔(.h)中。這種寫在類別模板定義中的成員函數會被隱式宣告為
inline 函數。
類別模板的成員函數定義可以在類別內部或外部完成:
template <typename T>
class MyClass {
public:
void display() {
std::cout << "Display function with type T" << std::endl;
}
};
template <typename T>
class MyClass {
public:
void display(); // 僅宣告
};
// 成員函數定義在類別外部
template <typename T>
void MyClass<T>::display() {
std::cout << "Display function with type T" << std::endl;
}類別模板的特化:
有時候,對於某些特定的資料型態,你可能希望提供一個特殊的類別實現,這就是所謂的類別模板特化。在特化中,你可以為特定的模板參數提供一個不同的實現。例如:
template <>
class Box<char> {
public:
char contents;
// 特殊的實現
};
template <>
class MyClass<int> {
public:
void display() {
std::cout << "Specialized display function for int" << std::endl;
}
};
template <typename T1, typename T2>
class MyClass {
public:
void display() {
std::cout << "Generic display" << std::endl;
}
};
// 局部特化版本: 允許根據部分模板參數進行特化,但只適用於類別模板(函數模板不支援局部特化)。
template <typename T>
class MyClass<T, int> {
public:
void display() {
std::cout << "Partially specialized display for second type as int" << std::endl;
}
};注意事項:
型別安全:類別模板增加了代碼的型別安全性,因為它們允許你在編譯時檢查型別錯誤。
程式碼複雜性:使用類別模板可能會使代碼變得更加複雜,尤其是在涉及多個模板參數和模板特化時。
編譯器相關:類別模板的行為可能會依賴於特定的編譯器和其模板實現,這可能會導致在不同編譯器間的兼容性問題。
模板用於『標準模板程式庫(STL)』和其他許多程式庫內,提供了強大的資料結構和演算法。
妥善使用類別模板可以大大提高程式碼的重用性和可維護性,但也需要開發者對模板有深入的理解,以避免引入不必要的複雜性。
透過精心設計與使用模板,可建立强大且靈活的 C++ 程式。
typename做為模板參數的型態指示符
在定義模板(無論是類別模板或是函數模板)時,typename
用來指定一個『泛型』型態参数。
用於告訴編譯器,隨後跟隨的標識符應被視爲一個『型態』。
typename 可與
class 關键字互换使用。
template <typename T>
class Box {
T value;
// ...
};
template <typename T>
void print(const T& value) {
std::cout << value << std::endl;
}用於指定依賴型態名稱
在模板中,當定義一個依赖於模板参数的型態时,需要使用
typename
来告訴編譯器,特定的標識符表示一個型態。這種情況通常發生在『模板內部』,尤其是在處理嵌套依赖行態时。
template <typename T>
class MyClass {
typename T::SubType value; // 使用 typename 指定 T::SubType 是一个类型
// ...
};T::SubType 可能是一個依赖於模板参数
T 的型態。由於編譯器在處理模板时無法確定
T::SubType 是否為一个型態,故
typename
關鍵字在此處是必须的,以避免歧義。
成員函数模板(Member Function Templates)允許在類別内部定義模板函數。
其意指這些成員函数可針對多種数据型態操作,而無需為每種型態編寫特定單獨的函数。
成员函数模板提高程式碼的靈活性與重用性,同時保持型態安全。
定義成員函數模板
成員函數模板的定義類似於普通模板函数的定義,但它們是在類別定義的内部。
成員函數模板可使用類別模板參數,也可以有自己的模板參數。例如:
class MyClass {
public:
// 成員函数模板
template <typename T>
void print(const T& value) {
std::cout << value << std::endl;
}
};
print
是一个成員函数模板,其可接受任何型態的參數並將其輸出。使用成員函數模板
使用成員函數模板與使用普通成員函数類似,但有時可能需要明確指定模板參數。
MyClass obj;
obj.print(123); // 打印整数
obj.print("Hello World"); // 打印字串成員函數模板與類別模板
template <typename U>
class MyClass {
public:
// 使用類別模板參數
void doSomething(const U& value) {
// ...
}
// 成員函数模板,有自己的模板參數
template <typename T>
void print(const T& value) {
std::cout << value << std::endl;
}
};注意事項
成員函數模板不能為虚函数(virtual)。
若成員函数模板和一般成員函数重載,則需注意函數的選擇和調用規則,可能需要明確指定模板參數以幣免歧義。
成員函數模板的使用提高程式碼的通用性,但也可能增加了複雜性,特别是在涉及模板嵌套與特化时。
一般來說,模板的定義必須放在可見的地方(通常放在標頭檔
.h
中),否則編譯器在看到使用處時不知道怎麼產生實際程式碼。這在大型專案中會導致:
編譯時間拉長:幾乎每個使用模板的翻譯單元,都會重新編譯模板定義。
程式碼膨脹:可能在不同翻譯單元中重複產生相同的模板實例。
模板顯式實例化(Explicit Template Instantiation)和模板宣告(Template Declaration)用於控制模板的編譯和連結方式。
模板顯式實例化
在 C++ 中,當使用模板定義一個函数或類別时,編譯器並不立即產生該模板的實際程式碼。相反,它會等到模板被實際使用時(例如,創建模板類別的物件或呼叫模板函數)時才會進行實例化。然而,有時能想要『顯式』告诉編譯器在某個特定點產生模板的實例化程式码,此即為所谓的『模板顯式實例化』。
有些模板非常龐大或內部實作複雜,編譯器在自動實例化時,需要做很多工作。如果在程式初期就能知道將來要用到哪幾種特定型別,可以直接在某個翻譯單元中顯式實例化,讓編譯器提早產生那些實體並完成編譯。這樣就不用在使用處再進行實例化,可能減少重複工作、縮短整體編譯時間。
顯式實例化能幫助你在單一翻譯單元集中生成所需的實體,減少在其他翻譯單元中重複定義的風險。
顯式實例化的語法是在模板定義前加上
template
關键字與模板實例的具體型態。例如:
template class MyClass<int>; // 顯式實例化 MyClass 的 int 版本
template void myFunction<double>(); // 顯式實例化 myFunction 的 double 版本模板宣告
模板宣告(也被稱為顯示實例化宣告(Explicit Instantiation Declaration))是一種只宣告模板實例而不定意它的方式。
『模板宣告』在分離編譯時非常有用。當在一個檔案中宣告模板的顯式實例化時,編譯器不會在該檔案中產生模板的實例化程式碼,但編譯器知道該模板的實例在程式的其他部分已經或將會被實例化。
模板宣告的語法是在模板定義前加上
extern template
關鍵字和模板實例的具體型態。例如:
extern template class MyClass<int>; // 顯式宣告 MyClass 的 int 版本
extern template void myFunction<double>(); // 顯式宣告 myFunction 的 double 版本使用場景
編譯時間:顯式實例化可减少編譯時間,因其避免在每個使用模板的原始檔中都進行模板的實例化。
控制實例化:透過顯式實例化與宣告,可更好地控制模板實例化的位置與方式,這在處理大型專案與程式庫時非常有用,尤其是在希望減少編譯相依與編譯時間的情况下。
避免連結錯誤:顯式實例化有助於防止連結錯誤,特别是在模板程式碼出現在多個檔案中時。
模板顯式實例化與宣告是 C++ 模板中用於管理和優化編譯過程的重要技巧。正確使用這些技術可提高編譯效率,減少編譯時間,並有助更好地組織與控制模板的結構。
using定義模板別名與顯式指定模板參數使用 using 定義模板别名
模板别名允許為複雜的模板型態提供更簡短、更易讀的名稱。
在處理『嵌套模板』或模板參數較多的情况下特别有用。
使用 using
關鍵字可創建這樣的别名。
template <typename T>
using Vec = std::vector<T>;
Vec<int> intVector; // 等同於 std::vector<int>使用 using 顯式指定模板參數
using
也可用於在類別模板內部顯式指定某個已有模板的參數,從而創建一個特定型態的别名。
這在設計模板類別或模板函數時特别有用,可以提高程式码的可讀性和便利性。例如:
template <typename T>
class MyClass {
public:
using VecType = std::vector<T>;
void doSomething() {
VecType myVector;
// ...
}
};
在此例,VecType
是一個依賴於模板參數 T 的
std::vector 型態的别名。
在 MyClass 的成員函数
doSomething 中,可直接使用
VecType 而不是
std::vector<T>。
在 C++ 模板中,全局特化(Full Specialization)和局部特化(Partial Specialization)是用於自定義模板行為的技術。其允許為特定型態或一組型態提供特定的模板實現。
這些特化技術在設計通用程式庫與框架時特别有用,因为它们可以優化與调整模板在特定情况下的行為。
全局特化(Full Specialization)
全局特化是指為模板定義一個特定型態的完全特定版本。
在全局特化中,模板的所有參數都被具體的型態替换。意指特化的版本將僅用於這些具體的型態。
全局特化適用於當所有模板參數都被特定类型替换时。
全局特化一般用於改變模板在特定型態下的行為。
template <typename T>
void print(T value) {
std::cout << value << std::endl;
}
// 可為 int 型態提供一個『全局特化版本』:
template <>
void print<int>(int value) {
std::cout << "Integer: " << value << std::endl;
}
// 在這個例子中,當 print 被呼叫且型態為 int 時,會使用特化的版本。局部特化(Partial Specialization)
局部特化是指為『類別模板』提供一個特化版本,其中一部分模板參數被具體型態替換,而其他參數仍然保持泛型。
允許對模板的特定應用提供更特定的行為。
局部特化则適用於只有部分模板參數被替换的情况。
需要注意的是,C++ 標準只允許對『類別模板』進行局部特化,而不允許對函数模板進行『局部特化』。
template <typename T, typename U>
class MyPair {
T first;
U second;
// ...
};
// 可為 T 為 int 的情況提供一個局部特化版本:
template <typename U>
class MyPair<int, U> {
int first;
U second;
// 特定於 int 的實現
};
// 在這個例子中,當 MyPair 的第一個型態參數是 int 時,會使用此局部特化的版本。可變參數模板(Variadic Templates)
可變參數模板允許定義『接受任意數量和型態』的模板參數的模板。
透過在模板參數列表中使用『省略号(...)』來實現的,其表明可以接受任意數量的模板參數。
可變參數模板廣泛用於創建通用的函数和類別,特别是在需要處理不確定數量的型態時。
template <typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // C++17 折叠表達式
}
template <typename... Args>:宣告這是可變參數模板,Args...
表示任意數量、任意型別的參數。呼叫 print(1, "hello", 3.14, std::string("C++"));
時,編譯器會根據形參推導(template argument deduction)出
Args 分別為
<int, const char*, double, std::string>。
(std::cout << ... << args) 為 C++17
引入的「折叠表達式」,可將左邊的運算(std::cout <<)對多個
args 進行逐項展開。
模板模板參數(Template Template Parameters)
模板模板參數則是指「把一個模板當作參數」傳遞給另一個模板」。它允許我們在定義一個模板時,要求「呼叫者必須提供另一個符合指定介面的模板」。
常見用法之一是為了抽象化容器類型,例如想寫一個模板類別,可以接受「任何模板形式的容器(像
std::vector、std::list,或自定義的
MyContainer)」並用於儲存某種型別 T 的資料。
模板模板參數是指在模板定義中,參數本身就是一個模板。
其允許編寫更靈活的程式碼,因可将模板作为參數傳遞给另一個模板。
模板模板參數常用於設計需要泛型容器型態作为参数的類別與函數。
#include <vector>
#include <list>
#include <iostream>
// template <template <class...> class Container, class T>
// ^^^^^^^^ 這裡是 "模板模板參數" 的關鍵字
// ↑————— 參數本身就是 "一個帶有型別參數可變數量" 的模板
template <template <class...> class Container, typename T>
class Wrapper {
public:
// 以提供的模板 'Container' 與型別 'T' 為基底,來實例化新的容器
Container<T> data;
Wrapper() = default;
void addElement(const T& value) {
data.push_back(value);
}
};
int main() {
// 以 std::vector 為模板傳入
Wrapper<std::vector, int> wv;
wv.addElement(10);
wv.addElement(20);
// 以 std::list 為模板傳入
Wrapper<std::list, int> wl;
wl.addElement(30);
wl.addElement(40);
// ...後續程式碼
return 0;
}
template <class...> class Container 表示此參數
Container 本身是一個模板,而且它的模板參數列表是
class...,表示可變數量的 class 型別(類似
std::vector, std::list, 等等)。typename T 表示要儲存的資料型別。结合使用
可將「模板模板參數」與「可變參數模板」結合使用。例如,想要允許容器本身能接受任意多個型別參數(譬如
std::map<Key, T, Compare, Alloc>
之類)。如此便能支援更多複雜的容器模板形式。可以寫成:
template <template <typename...> class Container, typename... Args>
class FlexibleWrapper {
public:
FlexibleWrapper() = default;
// 這裡用一個容器,容器的泛型參數就是 Args...
Container<Args...> data;可變參數模板
關鍵語法:typename... Args /
class... Ts + ... 展開。
用於:
函式的參數個數與型別都可能不同時(如通用的 print
函式)。
類別中需要封裝/儲存多種型別的結構(如
std::tuple)。
重點:聚焦在「不定數量的型別或函式參數」。
模板模板參數
關鍵語法:template <class...> class Container(或非可變參數的版本)。
用於:
需要在模板中「再注入一個模板」以進行二次定義,例如要接受「一個可存放 T 的容器模板」。
允許在設計階段,將「容器模板」或「Policy 類別模板(策略或是行為的封裝)」等抽象出來(與傳統的「繼承+虛函式」相比,模板注入(Policy 類別模板)方式可以在『編譯期』決定最終行為,省去執行期虛函數呼叫的額外成本,也更容易在不同實例之間做內聯或最佳化。)。
重點:聚焦在「將一個模板當作參數傳遞給另一個模板」,以實現模板組合或策略模式等設計。
可變參數模板(Variadic Templates)解決「我想寫一個可以接受任意數量/型別的函式或類別」的需求。