模板與泛型是現代 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<> 內有多少模板參數;主要是看函數模板內的函數名稱之後的參數數量。
EX:
template <typename T, typename U, typename W>
T add(T a, U b, W c) { return a + b + c; }
它有 3 個模板參數 (T, U, W)
但在呼叫時,C++ 不會先看這些 template 參數數量。
編譯器實例化一個模板時,其使用實際的模板實參代替對應的模板參數來建立一個新的『實例』。
它只看:
函數名稱:add
函數參數:add(a, b, c) → 三個參數
模板參數推導 (template argument deduction)
讓 C++ 自動 deduce T, U, W:
auto r = add(1, 2.0, 3);
C++ 自動推論:
T = int
U = double
W = int
所以:
呼叫時 根本不需要知道 template 裡面有幾個參數
呼叫時只需要 看函數參數列(function parameter list)
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, 13與int一致。
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)』可以是一個整數型態,或是一個指向物件或函數型態的『指標』或『左值參考』。
綁定到指標或參考非型態參數的實參必須具有『靜態』的生存期。
因為模板實例化在 編譯期,但物件的生命期若是在 執行期才決定,編譯器就無法保證:
這個物件會存在
它有固定的位址
它能成為編譯期常量
int func() {
int a = 5;
X<a> obj; // 不合法。a 是在 執行期 才建立,它的位址不固定,不符合「編譯期常數」的要求
}無法使用一個普通(非
static)局部變數,或動態物件作為指標或參考『非型態參數(nontype
parameter)』的實參。
非型態模板參數要求:
編譯期可得知
值具有固定記憶體位址或大小
推薦:const、constexpr、static
字面量如 "hi" → 其陣列長度為編譯期常數 →
可以當模板參數
『指標』或『參考』作為模板參數 → 被綁定或指向的物件必須具有「靜態生命期」
動態物件(new
產生的物件位址在『執行期』決定)或局部變數物件不符合要求,故無法使用inline
與 constexpr 的函數模板
函數模板可以宣告為 inline 或
constexpr(允許編譯期求值,保證函數在條件允許下於編譯期計算)。
為什麼需要 inline?
因為模板是『按需求實例化』,如果在多個翻譯(編譯)單元(translation units)使用相同模板實例,編譯器會在每個翻譯(編譯)單元會產生相同的函數實體,進而導致連結時發生 『multiple definition』 錯誤。
可允許同一個函數模板實體存在於多個翻譯單元,並允許連結器把它們視為同一個定義(ODR-friendly)
若模板定義放在 header 檔且同時被多個 cpp 檔所使用,則建議使用 inline
// 正確
template <typename T>
inline T min(const T&, const T&);
// 錯誤
// inline template <typename T>
// T min(const T&, const T&);函數模板的『特化』
有時可能需要對某『特定型態』提供特殊的實現,這就是所謂的『模板特化』。
『模板特化』就是告訴編譯器,當特定的型態被用作模板參數時,使用特定的函數來實現。
為某些特定型態提供不同的實作,但仍可保持『泛型函數模板』的介面不變。
完全特化語法固定為:
template <>
ReturnType function_name<SpecificType>(arguments) { ... }template <typename T>
T abs_value(T x) {
return x < 0 ? -x : x;
}
// 如果 T = int, double → 正常
// 但如果 T = std::complex → 不合法(無 < 運算符)
// 此時我們希望提供特定型態的版本:
template <>
std::complex<double> abs_value(std::complex<double> x) {
return std::abs(x);
}
template <>
void swap<double>(double& a, double& b) {
// 特殊處理 double 型態的 swap
}
注意事項:
型態推斷:在大多數情況下,編譯器可以自動推斷模板參數的型態。但有時可能需要手動指定型態,尤其是當函數無法從程式上下文中推斷出正確的型態時。
template <typename T>
T add(T a, T b) {
return a + b;
}
add(1, 2); // T 被推斷為 int
add(1.0, 2.5); // T 被推斷為 double
add(1, 2.5); // 錯誤:T 要同時是 int 與 double → 無法推斷
// 必須手動指定:
add<double>(1, 2.5); // 正確
// 從回傳值無法推斷
template <typename T>
T make_value();
auto x = make_value(); // 錯誤:無法推斷 T
int x = make_value<int>(); // 正確
過度泛化:雖然函數模板可以提高程式碼的重用性,但過度泛化可能會導致程式碼難以理解和維護。故適當且合理地使用模板對於寫出高品質的程式非常重要。
// 過度泛化
template <typename T>
bool equal(T a, T b) {
return a == b;
}
所有型態都允許比較,但某些型態根本不能定義良好的等號
可能產生編譯錯誤或語意錯誤
編譯錯誤:由於模板函數是在編譯時實例化的,所以任何與型態相關的錯誤都會在編譯時出現。這種錯誤有時可能難以理解,特別是在複雜的模板實例化中。
程式碼膨脹:每當使用新的型態實例化模板時,編譯器都會生成一份新的函數實現。如果濫用模板,可能導致執行檔的大小或顯著增加。
『類別模板』的主要目的是為了程式碼重用和型態安全。
編譯器無法為類別模板推論模板參數,故使用類別模板時,必須在模板名稱後面使用
<>
來提供額外資訊(模板參數列表內的參數)。
定義類別模板:
類別模板的定義以 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
來告訴編譯器,特定的標識符表示為一個『型態』。
這種情況通常發生在『模板內部』,尤其是在處理『嵌套相依型態』時。
T::SubType 就是一個典型例子:
是一個型態
是一個靜態成員變數或常量
而編譯器在看到模板時 還不知道 T
是什麼,因此無法判斷 T::SubType
應被當成『型態解析』或是『值解析』?
template <typename T>
class MyClass {
typename T::SubType value; // 使用 typename 指定 T::SubType 是一个类型
// ...
};
T::SubType 可能是一個相依於模板参数
T 的型態。
由於編譯器在處理模板時無法確定
T::SubType 是否應視為型態,故
typename
關鍵字在此處是必要的,以避免歧義。
只要型態名稱 依賴模板參數 就一定要寫
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)。
虛函數的動態繫結需要求「確定的函數簽名」
vtable 需要在編譯期間(compile
time)就要確定每個虛函數的「確切函數簽名」
vtable要求:
每個虛函數意指唯一的函數指標值(函數記憶體位址)
在編譯期就要加入 vtable
但『函數模板』並沒有固定簽名。
函數模板並非真正的函數:
那「類別模板可以有虛函數」可以嗎?
可以。
template <typename T>
class Base {
public:
virtual void func() {} // 合法:虛函數本身不是模板
};成員函數模板的使用提高程式碼的通用性,但也可能增加了複雜性,特别是在涉及模板嵌套與特化时。
一般來說,模板的定義必須放在可視的範圍(通常放在標頭檔
.h
中),否則編譯器在看到模板使用處時不知該如何產生實際程式碼。
故在大型專案中會導致:
編譯時間拉長:幾乎每個使用模板的編譯單元,都會重新編譯模板定義。
程式碼膨脹:可能在不同編譯單元中重複產生相同的模板實例。
模板顯式實例化(Explicit Template Instantiation)和模板宣告(Template Declaration)用於控制模板的編譯和連結方式。
模板顯式實例化(explicit instantiation definition)
在 C++ 中,當使用模板定義一個函数或類別时,編譯器並不立即產生該模板的實際程式碼。相反,它會等到模板被實際使用時(例如,創建模板類別的物件或呼叫模板函數)時才會進行實例化。
有些模板非常龐大或內部實作複雜,編譯器在自動實例化時,需要做很多工作。
如果在程式初期就能知道將來要用到哪幾種特定型別,可以直接在某個翻譯單元中顯式實例化,讓編譯器『提早』產生那些實體並完成編譯。
這樣就不用在使用處再進行實例化,其可以減少重複工作並縮短整體編譯的時間。
故顯式實例化能幫助你在單一翻譯單元集中生成所需的實體,減少在其他翻譯單元中重複定義的風險。
顯式實例化的語法:
template
關键字與模板實例的具體型態。例如:// MyClass.h
template <typename T>
class MyClass; // 只有宣告,沒有定義
// MyClass.cpp
#include "MyClass.h"
template class MyClass<int>; // 錯誤
// MyClass.h
template <typename T>
class MyClass {
public:
void foo();
};
// MyClass.cpp
#include "MyClass.h"
template <typename T>
void MyClass<T>::foo() {}
// 模板定義「完整可見」
template class MyClass<int>;
template <typename T>
void myFunction(T);
template void myFunction<int>(int); // 錯誤:沒有定義,無法實例化
template <typename T>
void myFunction(T x) {}
template void myFunction<int>(int); // OK模板宣告
模板宣告(也被稱為顯示實例化宣告(Explicit Instantiation Declaration))是一種只宣告模板實例而不定義它的方式。
『模板宣告』在分離編譯時非常有用。當在一個檔案中宣告模板的顯式實例化時,編譯器不會在該檔案中產生模板的實例化程式碼,但編譯器知道該模板的實例在程式的其他部分已經或將會被實例化。
模板宣告的語法是在模板定義前加上
extern template
關鍵字和模板實例的具體型態。例如:
extern template class MyClass<int>; // 顯式宣告 MyClass 的 int 版本
extern template void myFunction<double>(); // 顯式宣告 myFunction 的 double 版本使用場景
編譯時間:顯式實例化可减少編譯時間,因其避免在每個使用模板的原始檔中都進行模板的實例化。
控制實例化:透過顯式實例化與宣告,可更好地控制模板實例化的位置與方式,這在處理大型專案與程式庫時非常有用,尤其是在希望減少編譯相依與編譯時間的情况下。
避免連結錯誤:顯式實例化有助於防止連結錯誤,特别是在模板程式碼出現在多個檔案中時。
模板顯式實例化與宣告是 C++ 模板中用於管理和優化編譯過程的重要技巧。正確使用這些技術可提高編譯效率,減少編譯時間,並有助更好地組織與控制模板的結構。
使用 using 定義『模板别名』
模板别名允許為複雜的模板型態提供更簡短、更易讀的名稱。
在處理『嵌套模板』或模板參數較多的情况下特别有用。
使用 using
關鍵字可創建這樣的别名。
typedef 無法為模板建立別名。
template <typename T>
using Vec = std::vector<T>;
Vec<int> intVector; // 等同於 std::vector<int>,形成一個具體型別(type formation),會觸發「隱式實例化」使用 using 顯式指定模板參數
using
也可用於在類別模板內部顯式指定某個已有模板的參數,從而創建一個特定型態的别名。
這在設計模板類別或模板函數時特别有用,可以提高程式码的可讀性和便利性。例如:
template <typename T>
class MyClass {
public:
using VecType = std::vector<T>;
void doSomething() {
VecType myVector;
// ...
}
};
在此例,VecType
是一個依賴於模板參數 T 的
std::vector 型態的别名。
VecType 在類別範圍內等價於
std::vector<T>
在 成員函数 doSomething
中,可直接使用 VecType 而不是
std::vector<T>。
由於 VecType
僅為型別層級的別名,此定義不會觸發任何模板實例化。
嵌套模板的最佳用法
template <typename T>
using VecMap = std::map<std::string, std::vector<T>>;
VecMap<int> x; // std::map<string, vector<int>>
VecMap<double> y; // std::map<string, vector<double>>
// 沒有 using 時,你需要寫出一大串複雜語法:
std::map<std::string, std::vector<int>> x;
若未來改成:std::map<std::string, std::deque<T>>,只需改一行,後面使用的部分不需修改。
template <typename T>
using VecMap = std::map<std::string, std::deque<T>>;對於深度嵌套的模板型別,應優先使用 using
建立模板別名,以封裝語意並降低閱讀與維護成本。
在 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 時,會使用此局部特化的版本。
template <typename T>
struct Traits {
static constexpr bool is_pointer = false;
};
// 局部特化:任何指標型別
template <typename T>
struct Traits<T*> {
static constexpr bool is_pointer = true;
};函數模板僅支援全局特化,不支援局部特化,其相近效果通常以函數重載實現。
QuantLib 用 partial specialization 分類型別,用 full specialization 精準鎖定型別。
可變參數模板(Variadic Templates)
可變參數模板允許模板參數列表中包含 零個、任意個數 的型別或非型別參數。
透過在模板參數列表中使用『...』來實現的,其表明可以接受任意數量的模板參數。
可變參數模板廣泛用於創建通用的函数和類別,特别是在需要處理不確定數量的型態時。
template <typename ... T>
void myfunct1(T ... args) // args: 可變形式參數,代表一包參數 // T ... : T稱為 『可變參數型態』
{
std::cout << sizeof...(args) << std::endl;
// std::cout << sizeof...(T) << std::endl;
}
myfunct1(); // 0
myfunct1(10, 20); // 2
myfunct1(10, 2.2, "abc", 68); // 4template <typename T, typename ... U>
void myfunct2(const T& firstarg, const U& ... otherargs)
{
std::cout << sizeof... (firstarg) << std::endl; // 錯誤:sizeof...只能用在一包型態或一包形參
std::cout << sizeof... (otherargs) << std::endl;
}
myfunct2(); // 錯誤
myfunct2(10); // 0
myfunct2(10, "abc", 12.5); // 2 參數包的展開(解包)
一般都是用遞迴函數的方式展開參數包。
一個 『參數包展開函數』 與 一個『同名遞迴終止函數』。
這種「遞迴 + base case」寫法是 C++11/14 時代的典型技巧。
#include <iostream>
void myfunc2()
{
std::cout << "參數包展開執行了遞迴終止函數 myfunc2()" << std::endl;
}
template <typename T, typename... U>
void myfunc2(const T& firstarg, const U&... otherargs)
{
std::cout << "收到的參數值為: " << firstarg << std::endl;
myfunc2(otherargs...); // 遞迴展開參數包
}
int main()
{
myfunc2(42, 3.14, "Hello", 'A');
}
// C++ 17之後的寫法:折叠表達式
template <typename... Args>
void myfunc2(Args&&... args)
{
(std::cout << ... << args) << std::endl;
}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)
模板的參數本身就是「一個模板」。
語法:
template <template <class...> class Container, typename T>
class Wrapper { ... };
template <class...>
Container 是一個帶有「型別參數包」的模板(例如
vector<T>、list<T>)。
Container 是一個模板類別,其模板參數列表接受「任意多個 class 型參」
class Container
Container 必須可以被實例化為:
Container<T>,因此,你可以傳入:std::vector、std::list,任何自定義的
template <typename...> class MyContainer用途:
- 對容器類型進行抽象(Vector vs List vs 自訂容器)
- 寫出高度泛用的資料結構
- 支援政策模式(Policy-based design)
將類別的行為拆成「多個獨立的政策(Policy Classes)」
再透過 模板參數(尤其是模板模板參數) 將這些政策組合起來
在「編譯期」決定類別的行為,而非「執行期」
模板模板參數則是指「把一個模板當作參數」傳遞給另一個模板」。它允許我們在定義一個模板時,要求「呼叫者必須提供另一個符合指定介面的模板」。
常見用法之一是為了抽象化容器類型,例如想寫一個模板類別,可以接受「任何模板形式的容器(像
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)解決「我想寫一個可以接受任意數量/型別的函式或類別」的需求。
模板模板參數(Template Template Parameters)則解決「我想寫一個模板,需要把另一個模板(而非具體型別)傳進來」的需求。