类模板(参考《C++ Templates 英文版第二版》)
類模板(參考《C++ Templates 英文版第二版》)
Chapter 1 類模板
與函數相似,類也可以被一個或者多個類型參數化
在這章,我們使用棧作為例子
2.1 類模板stack的實現
#include <vector> #include <cassert>template<typename T> class Stack { private:std::vector<T> elems;public:void push(T const& elem);void pop();T const& top() const;bool empty() const{return elems.empty();} };template <typename T> void Stack<T>::push(T const& elem) {elems.push_back(elem); }template <typename T> void Stack<T>::pop() {assert(!elems.empty());elems.pop_back(); }template <typename T> T const& Stack<T>::top() const {assert(!elems.empty());return elems.back(); }這個類模板通過一個STL里面的類模板vector<>實現.這樣,我們就不用去實現內存管理,拷貝構造,賦值運算符等內容,專注于類的實現
2.1.2 成員函數的實現
如果你要定義一個類模板的成員函數,你必須去指定他是一個模板,并且滿足類模板的全部類型,就像下面這樣:
void Stack<T>::push(T const& elem) {elems.push_back(elem); }在這種情況下,push_back()被調用,向vector添加一個elem
注意:pop_back()函數只移除最后一個元素,但是不返回它,只是因為這種行為(只移除)是異常安全的,是不可能實現一個移除并返回最后一個元素的異常安全函數的1
2.2 stack實用類模板
#include "max1.hpp" #include <iostream> #include <string> #include <format> #include <type_traits> #include "stack1.hpp" #include <iostream> #include <string>int main() {Stack<int> intStack; // stack of intsStack<std::string> stringStack; // stack of strings// manipulate int stackintStack.push(7);std::cout << intStack.top() << '\n';// manipulate string stackstringStack.push("hello");std::cout << stringStack.top() << '\n';stringStack.pop(); }C++17可以這么寫2
#include <vector> int main() {std::vector intVector{ 1,2 }; //省略<> }注意:類模板只會實例化被調用的函數,再上個例子中,int string的top(),push()都被實例化,但是pop()只在傳入string實例化一次
2.3 類模板的部分使用(Partial Usage of Class Templates)
一個類模板通常對模板參數提供多種操作,這可能會給你錯覺:類模板必須提供模板參數所有成員函數的操作.
但事實上不是這樣,類模板只會提供被模板參數用到的成員函數
在上文中加入以下代碼:
void printOn(std::ostream& strm){for (T const& elem : elems){strm << elem << std::endl;}}運行:
Stack<std::pair<int, int>> ps;ps.push({ 2,5 });ps.push({ 3,5 });std::cout << ps.top().first << std::endl; //正確//ps.printOn(std::cout); // 錯誤只有當你調用printOn()時,才會報錯,說明類模板只會實例化需要的成員函數,
2.3.1 概念(Concepts)
類模板、函數模板及非模板函數(常為類模板成員)可以與制約關聯,制約指定模板實參上的要求,這能用于選擇最準確的函數重載和模板特化。
制約亦可用于限制變量聲明和函數返回類型中的自動類型推導,為只有滿足指定要求的類型。
這種要求的具名集合被稱為概念。每個概念都是謂詞,于編譯時求值,并成為模板接口的一部分,它在其中用作制約:
#include <locale> #include <string> using namespace std::literals;// 概念 "EqualityComparable" 的聲明,任何有該類型值 a 和 b , // 而表達式 a==b 可編譯而其結果可轉換為 bool 的 T 類型滿足它 template <typename T> concept bool EqualityComparable = requires(T a, T b) {{a == b} -> bool; };void f(EqualityComparable&&); // 有制約函數模板的聲明 // template<typename T> // void f(T&&) requires EqualityComparable<T>; // 相同的長形式int main() {f("abc"s); // OK : std::string 為 EqualityComparablef(std::use_facet<std::ctype<char>>(std::locale{})); // 錯誤:非 EqualityComparable }更多請參見:制約與概念 - cppreference.com
2.4 友元
與其使用printOn函數打印元素,不如重載operator<<,然而通常operator<<會實現為非成員函數。下面在類內定義友元,它是一個普通函數
template<typename T> class Stack {...void printOn(std::ostream& os) const;friend std::ostream& operator<<(std::ostream& os, const Stack<T>& stack){stack.printOn(os); return os;} };如果在類外定義友元,類模板參數不可見,事情會復雜很多
template<typename T> class Stack {...friend std::ostream& operator<<(std::ostream&, const Stack<T>); };std::ostream& operator<<(std::ostream& os,const Stack<T>& stack) // 錯誤:類模板參數T不可見 {stack.printOn(os);return os; }有兩個解決方案:
-
一是隱式聲明一個新的函數模板,并使用不同的模板參數
template<typename T> class Stack {… template<typename U> friend std::ostream& operator<<(std::ostream&, const Stack<U>&); };// 類外定義 template<typename U> std::ostream& operator<<(std::ostream& os, const Stack<U>& stack) {stack.printOn(os);return os; } -
二是將友元前置聲明為模板,而友元參數中包含類模板,這樣就必須先前置聲明類模板
template<typename T> // operator<<中參數中要求Stack模板可見 class Stack;template<typename T> std::ostream& operator<<(std::ostream&, const Stack<T>&);// 隨后就可以將其聲明為友元 template<typename T> class Stack {…friend std::ostream& operator<< <T> (std::ostream&, const Stack<T>&); };// 類外定義 template<typename T> std::ostream& operator<<(std::ostream& os, const Stack<T>& stack) {stack.printOn(os);return os; }
同樣,函數只有被調用到時才實例化,元素沒有定義operator<<時也可以使用這個類,只有調用operator<<時才會出錯
Stack<std::pair<int, int>> s; // std::pair沒有定義operator<< s.push({1, 2}); // OK s.push({3, 4}); // OK std::cout << s.top().first << s.top().second; // 34 std::cout << s << '\n'; // 錯誤:元素類型不支持operator<<2.5 類模板特化
模板的實際應用中,有一些概念和應用很容易讓人混淆,現在就分析一下模板的特化和實例化。編寫模板的代碼,最終的目的是應用,而在實際應用的過程中,大家最經常使用的是模板的實例化。也就是說模板說一族類或函數的抽象,那么要使用它,就需要把它應用到某個具體的類或者函數上。
另外還有一個繞不開的就是:特化(specialization),從目前的教材來看有兩種理解:
- 凡是把模板用具體的值來替代的過程都叫特化。如果這么理解,實例化也是特化的一種。
- 特化是普通模板通過具體的值來替換后不能滿足一些特定的情況下的要求,需要對其進行特別的處理,包括偏特化和全特化。
全特化寫法
template<> class Stack<std::string> {... };實例:
#include "stack1.hpp" #include <deque> #include <string> #include <cassert>template<> class Stack<std::string> {private:std::deque<std::string> elems; // elementspublic:void push(std::string const&); // push elementvoid pop(); // pop elementstd::string const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();} };void Stack<std::string>::push (std::string const& elem) {elems.push_back(elem); // append copy of passed elem }void Stack<std::string>::pop () {assert(!elems.empty());elems.pop_back(); // remove last element }std::string const& Stack<std::string>::top () const {assert(!elems.empty());return elems.back(); // return copy of last element }當我們使用std::string作為模板參數時,就會實例化這個用std::string特化的類.
我的理解:模板的特化就是為了處理一些非一般情況
2.6 偏特化
函數是沒有偏特化的。所以這里只是介紹類的偏特化。所謂偏特化,又叫局部特化或者部分特化,也就是在特定的條件下使用特定的對象來替換模板參數,但又不能完全替換
#include "stack1.hpp"// partial specialization of class Stack<> for pointers: template<typename T> class Stack<T*> {private:std::vector<T*> elems; // elementspublic:void push(T*); // push elementT* pop(); // pop elementT* top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();} };template<typename T> void Stack<T*>::push (T* elem) {elems.push_back(elem); // append copy of passed elem }template<typename T> T* Stack<T*>::pop () {assert(!elems.empty());T* p = elems.back();elems.pop_back(); // remove last elementreturn p; // and return it (unlike in the general case) }template<typename T> T* Stack<T*>::top () const {assert(!elems.empty());return elems.back(); // return copy of last element }使用
template<typename T> class Stack<T*> {}我們定義了一個類模板
T仍然是模板參數,但是為了T*特化
具有多個參數的部分特化
template<typename T1, typename T2> class MyClass { … }; // partial specialization: both template parameters have same type template<typename T> class MyClass<T,T> { … }; // partial specialization: second type is int template<typename T> class MyClass<T,int> { … };// partial specialization: both template parameters are pointer types template<typename T1, typename T2> class MyClass<T1*,T2*> { … }; MyClass< int, float> mif; // uses MyClass<T1,T2> MyClass< float, float> mff; // uses MyClass<T,T> MyClass< float, int> mfi; // uses MyClass<T,int> MyClass< int*, float*> mp; // uses MyClass<T1*,T2*>如果有多個模板匹配,就會歧義:
MyClass< int, int> m; // ERROR: matches MyClass<T,T> and MyClass<T,int> MyClass< int*, int*> m; // ERROR: matches MyClass<T,T> and MyClass<T1*,T2*>2.7 默認模板參數
對類模板,你可以設置一個默認模板參數,例如,對于Stack你可以設置一個默認模板參數管理內容
template<typename T, typename Cont = std::vector<T>> class Stack {private:Cont elems; // elementspublic:void push(T const& elem); // push elementvoid pop(); // pop elementT const& top() const; // return top elementbool empty() const { // return whether the stack is emptyreturn elems.empty();} };template<typename T, typename Cont> void Stack<T,Cont>::push (T const& elem) {elems.push_back(elem); // append copy of passed elem }template<typename T, typename Cont> void Stack<T,Cont>::pop () {assert(!elems.empty());elems.pop_back(); // remove last element }template<typename T, typename Cont> T const& Stack<T,Cont>::top () const {assert(!elems.empty());return elems.back(); // return copy of last element }這個例子就是使用std::vector作為默認內容管理器
注意:這個類現在有兩個模板參數,所以每個成員函數也應該有兩個模板參數
int main() {// stack of ints:Stack<int> intStack;// stack of doubles using a std::deque<> to manage the elementsStack<double,std::deque<double>> dblStack;// manipulate int stackintStack.push(7);std::cout << intStack.top() << '\n';intStack.pop();// manipulate double stackdblStack.push(42.42);std::cout << dblStack.top() << '\n';dblStack.pop(); }這樣Stack<int> intStack;使用std::vector管理元素,你也可以自定義管理器Stack<double,std::deque<double>> dblStack;
2.8 類型別名
為整個類型定義一個新名字讓類模板更方便使用
通過使用using
using IntStack = Stack <int>; // alias declaration void foo (IntStack const& s); // s is stack of ints IntStack istack[10]; // istack is array of 10 stacks of ints using IntStack = Stack <int>;這樣你就可以為整個類型定義一個別名,更方便使用
由于模板不是一個類型,所以不能定義一個typedef引用一個模板,但是新標準(since C++11)允許使用using為類模板定義一個別名
以Stack使用std::deque`管理元素為例
template<typename T> using DequeStack = Stack<T, std::deque<T>>;這樣我們就可以使用DequeStack<int> 代替 Stack<int,std::deque<int>>,這兩個表達的完全相同
2.9 類模板參數推斷
在C++17之前,你必須傳入所有的模板參數類型,但是,C++17之后,這個限制放松了,如果能通過構造函數推斷出模板參數類型,你可以不用明確的指定參數類型
Stack< int> intStack1; // stack of strings Stack< int> intStack2 = intStack1; // OK in all versions Stack intStack3 = intStack1; // OK since C++17構造函數:
template<typename T> class Stack { private: std::vector<T> elems; // elementspublic: Stack () = default; Stack (T const& elem) // initialize stack with one element: elems({elem}) { }… };你可以這樣聲明一個Stack:
Stack intStack = 0; // Stack<int> deduced since C++17通過用整初始化Stack,推斷出模板參數T為int,從而實例化一個Stack<int>
原則上也可以傳遞字符串字面值常量,但這樣會造成許多麻煩。用引用傳遞模板類型T的實參時,模板參數不會decay,最終得到的類型是原始數組類型
Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17傳值的話則不會有這種問題,模板實參會decay,原始數組類型會轉換為指針
template<typename T> class Stack {public:Stack(T x) : v({x}) {}private:std::vector<T> v; };Stack stringStack = "bottom"; // Stack<const char*> deduced since C++17傳值時最好使用std::move以避免不必要的拷貝
template<typename T> class Stack {public:Stack(T x) : v({std::move(x)}) {}private:std::vector<T> v; };**推斷指引(**Deduction Guides)
如果構造函數不想使用傳值方式聲明,也有其他的解決辦法,
你可以定義一個專用的類型指引,將C字符串推斷為std::string
Stack( char const*) -> Stack<std::string>;這個指引必須出現在類定義的塊,或者命名空間里
Stack(const char*) -> Stack<std::string>; Stack stringStack{"bottom"}; // OK: Stack<std::string> deduced since C++17但由于語法限制,下面這種方法不可以
Stack stringStack = "bottom"; // Stack<std::string> deduced, but still not valid因為不能使用拷貝構造(=) 傳遞一個字符串去構造一個std::string.
你可以這樣:
Stack stack2{stringStack}; // Stack<std::string> deduced Stack stack3(stringStack); // Stack<std::string> deduced Stack stack4 = {stringStack}; // Stack<std::string> deduced2.10 模板化聚合(Templatized Aggregates)
聚合類也能作為模板
template<typename T> struct A {T x;std::string s; };這樣可以為了參數化值而定義一個聚合,它可以像其他類模板一樣聲明對象,同時當作一個聚合使用
A<int> a; a.x = 42; a.s = "initial value";C++17中可以為聚合類模板定義deduction guide
template<typename T> struct A {T x;std::string s; };A(const char*, const char*) -> A<std::string>;int main() {A a = { "hi", "initial value" };std::cout << a.x; // hi }沒有deduction guide,初始化就無法進行,因為A沒有構造函數來推斷。std::array也是一個聚合,元素類型和大小都是參數化的,C++17為其定義了一個deduction guide
namespace std { template<typename T, typename... U> array(T, U...)-> array<enable_if_t<(is_same_v<T, U> && ...), T>, (1 + sizeof...(U))>; }std::array a{ 1, 2, 3, 4 }; // 等價于 std::array<int, 4> a{ 1, 2, 3, 4 };累死了,中秋還在肝…
參考解答: GotW #8: CHALLENGE EDITION: Exception Safety ??
C++17之后,如果參數類型可以從構造函數推斷出來,可以跳過寫模板參數即 ??
總結
以上是生活随笔為你收集整理的类模板(参考《C++ Templates 英文版第二版》)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 收集20个经典的Java面试题
- 下一篇: C++入门