範本是 C++ 中泛型程式設計的基礎。 作為強型別語言,C++ 要求所有變數都有特定型別,由程式設計人員明確宣告或由編譯器推算。 不過,無論其運作的型別為何,許多資料結構和演算法看起來都相同。 範本可讓您定義類別或函式的作業,以及讓使用者指定那些作業應該處理的具體型類。
定義和使用範本
範本是一種建構,根據使用者為範本參數提供的引數,在編譯時間產生一般型別或函式。 例如,您可以定義如下所示的函式範本:
template <typename T>
T minimum(const T& lhs, const T& rhs)
{
return lhs < rhs ? lhs : rhs;
}
上述程式碼描述具有單一型別參數 T 之泛型函式的範本,其傳回值和呼叫參數 (lhs 和 rhs) 都是此類型。 您可以命名任何您想要的型別參數,但依照慣例,最常使用單一大寫字母。 T 是範本參數;typename
關鍵字表示此參數是類型的預留位置。 呼叫函式時,編譯器會將 T
的每個執行個體取代為由使用者指定或由編譯器推算的具體型別引數。 編譯器從範本產生類別或函式的程緒稱為範本具現化;minimum<int>
是範本 minimum<T>
的具現化。
在其他地方,使用者可以宣告針對 int 特殊化的範本執行個體。假設 get_a() 和 get_b() 是傳回 int 的函式:
int a = get_a();
int b = get_b();
int i = minimum<int>(a, b);
不過,由於這是函式範本,且編譯器可以從引數 a 和 b 推算 T
的型別,所以您可以像一般函式一樣呼叫它:
int i = minimum(a, b);
當編譯器遇到最後一個陳述式時,它會產生新的函式,而範本中每次出現的 T 都會取代為 int
:
int minimum(const int& lhs, const int& rhs)
{
return lhs < rhs ? lhs : rhs;
}
編譯器如何在函式範本中執行型別推算的規則,是以一般函式的規則為基礎。 如需詳細資訊,請參閱函數範本呼叫的多載解析。
型別參數
在上述 minimum
範本中,請注意型別參數 T 在函式呼叫參數中使用之前,不會以任何方式限定,其中會新增 const 和參考限定詞。
型別參數的數目沒有實際限制。 以逗號分隔多個參數:
template <typename T, typename U, typename V> class Foo{};
關鍵字 class
相當於此內容中的 typename
。 您可以將上述範例表示為:
template <class T, class U, class V> class Foo{};
您可以使用省略符號運算子 (...) 來定義採用任意數目零或多個型別參數的範本:
template<typename... Arguments> class vtclass;
vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
任何內建或使用者定義型別都可以當做型別引數使用。 例如,您可以在標準程式庫中使用 std::vector 來儲存型別為 int
、double
、std::string、MyClass
、const
MyClass
*、MyClass&
等的變數。 使用範本時的主要限制是型別引數必須支援套用至型別參數的任何作業。 例如,如果我們使用 MyClass
呼叫 minimum
,如下列範例所示:
class MyClass
{
public:
int num;
std::wstring description;
};
int main()
{
MyClass mc1 {1, L"hello"};
MyClass mc2 {2, L"goodbye"};
auto result = minimum(mc1, mc2); // Error! C2678
}
系統會產生編譯器錯誤,因為 MyClass
未提供 < 運算子的多載。
沒有任何固有需求,任何特定範本的型別引數全都屬於相同的物件階層,不過您可以定義強制執行此類限制的範本。 您可以將物件導向技術與範本結合;例如,您可以將 Derived* 儲存在向量 <Base*> 中。 請注意,引數必須是指標
vector<MyClass*> vec;
MyDerived d(3, L"back again", time(0));
vec.push_back(&d);
// or more realistically:
vector<shared_ptr<MyClass>> vec2;
vec2.push_back(make_shared<MyDerived>());
std::vector
和其他標準程式庫容器對 T
元素施加的基本需求是 T
為可指派複本且可建構複本。
非型別參數
不同於其他語言如 C# 和 JAVA 中的泛型型別,C++範本支援非型別參數,也稱為值參數。 例如,您可以提供常數整數值來指定陣列的長度,如同這個範例,與 Standard 程式庫中的 std::array 類別類似:
template<typename T, size_t L>
class MyArray
{
T arr[L];
public:
MyArray() { ... }
};
請注意範本宣告中的語法。 此 size_t
值會在編譯時間以範本引數的形式傳入,且必須是 const
或 constexpr
運算式。 您可以使用它,如下所示:
MyArray<MyClass*, 10> arr;
其他類型的值,包括指標和參考,皆能以非型別參數的形式傳入。 例如,您可以傳入函式或函式物件的指標,以自訂範本程式碼內的某些作業。
非型別範本參數的型別推算
在 Visual Studio 2017 和更新版本中,在 /std:c++17
模式或更新版本中,編譯器會推算以 auto
宣告的非型別範本引數型別:
template <auto x> constexpr auto constant = x;
auto v1 = constant<5>; // v1 == 5, decltype(v1) is int
auto v2 = constant<true>; // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>; // v3 == 'a', decltype(v3) is char
範本作為範本參數
範本可以是範本參數。 在此範例中,MyClass2 有兩個範本參數:typename 參數 T 和範本參數 Arr:
template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
T t; //OK
Arr<T, 10> a;
U u; //Error. U not in scope
};
因為 Arr 參數本身沒有本文,因此不需要其參數名稱。 事實上,從 MyClass2
本文內參照 Arr 的 typename 或類別參數名稱是錯誤的。 因此,可以省略 Arr 的型別參數名稱,如下列範例所示:
template<typename T, template<typename, int> class Arr>
class MyClass2
{
T t; //OK
Arr<T, 10> a;
};
預設範本引數
類別和函式範本可以有預設引數。 當範本有預設引數時,您可以在使用範本時將其保留為未指定。 例如,std::vector 範本具有配置器的預設引數:
template <class T, class Allocator = allocator<T>> class vector;
在大部分情況下,預設 std::allocator 類別是可接受的,因此您可以使用如下的向量:
vector<int> myInts;
但如有必要,您可以指定自訂配置器,如下所示:
vector<int, MyAllocator> ints;
若有多個樣板引數,則第一個預設引數之後的所有引數都必須有預設引數。
使用所有參數皆為預設的範本時,請使用空白角括弧:
template<typename A = int, typename B = double>
class Bar
{
//...
};
...
int main()
{
Bar<> bar; // use all default type arguments
}
範本特製化
在某些情況下,無法或不建議讓範本針對任何型別定義完全相同的程式碼。 例如,您可能想要定義只有在型別引數是指標、std::wstring 或衍生自特定基底類別的型別時,才會執行的程式碼路徑。 在這種情況下,您可以定義該特定型別的範本特殊化。 當使用者具現化具有該型別的範本時,編譯器會使用特殊化來產生 類別,而針對所有其他型別,編譯器會選擇較一般的範本。 特殊化的所有參數都是完整的特殊化。 如果只有部分參數是特殊化的,則稱為部分特殊化。
template <typename K, typename V>
class MyMap{/*...*/};
// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization
只要每個特殊化型別參數是唯一的,範本就可以有任意數目的特殊化。 只有類別範本可以部分特殊化。 範本的所有完整和部分特殊化都必須在與原始範本相同的命名空間中宣告。
如需詳細資訊,請參閱範本特殊化。