类模板(参考《C++ Templates 英文版第二版》)
類模板(參考《C++ Templates 英文版第二版》)
Chapter 1 類模板
與函數(shù)相似,類也可以被一個(gè)或者多個(gè)類型參數(shù)化
在這章,我們使用棧作為例子
2.1 類模板stack的實(shí)現(xiàn)
#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(); }這個(gè)類模板通過一個(gè)STL里面的類模板vector<>實(shí)現(xiàn).這樣,我們就不用去實(shí)現(xiàn)內(nèi)存管理,拷貝構(gòu)造,賦值運(yùn)算符等內(nèi)容,專注于類的實(shí)現(xiàn)
2.1.2 成員函數(shù)的實(shí)現(xiàn)
如果你要定義一個(gè)類模板的成員函數(shù),你必須去指定他是一個(gè)模板,并且滿足類模板的全部類型,就像下面這樣:
void Stack<T>::push(T const& elem) {elems.push_back(elem); }在這種情況下,push_back()被調(diào)用,向vector添加一個(gè)elem
注意:pop_back()函數(shù)只移除最后一個(gè)元素,但是不返回它,只是因?yàn)檫@種行為(只移除)是異常安全的,是不可能實(shí)現(xiàn)一個(gè)移除并返回最后一個(gè)元素的異常安全函數(shù)的1
2.2 stack實(shí)用類模板
#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 }; //省略<> }注意:類模板只會(huì)實(shí)例化被調(diào)用的函數(shù),再上個(gè)例子中,int string的top(),push()都被實(shí)例化,但是pop()只在傳入string實(shí)例化一次
2.3 類模板的部分使用(Partial Usage of Class Templates)
一個(gè)類模板通常對(duì)模板參數(shù)提供多種操作,這可能會(huì)給你錯(cuò)覺:類模板必須提供模板參數(shù)所有成員函數(shù)的操作.
但事實(shí)上不是這樣,類模板只會(huì)提供被模板參數(shù)用到的成員函數(shù)
在上文中加入以下代碼:
void printOn(std::ostream& strm){for (T const& elem : elems){strm << elem << std::endl;}}運(yùn)行:
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); // 錯(cuò)誤只有當(dāng)你調(diào)用printOn()時(shí),才會(huì)報(bào)錯(cuò),說明類模板只會(huì)實(shí)例化需要的成員函數(shù),
2.3.1 概念(Concepts)
類模板、函數(shù)模板及非模板函數(shù)(常為類模板成員)可以與制約關(guān)聯(lián),制約指定模板實(shí)參上的要求,這能用于選擇最準(zhǔn)確的函數(shù)重載和模板特化。
制約亦可用于限制變量聲明和函數(shù)返回類型中的自動(dòng)類型推導(dǎo),為只有滿足指定要求的類型。
這種要求的具名集合被稱為概念。每個(gè)概念都是謂詞,于編譯時(shí)求值,并成為模板接口的一部分,它在其中用作制約:
#include <locale> #include <string> using namespace std::literals;// 概念 "EqualityComparable" 的聲明,任何有該類型值 a 和 b , // 而表達(dá)式 a==b 可編譯而其結(jié)果可轉(zhuǎn)換為 bool 的 T 類型滿足它 template <typename T> concept bool EqualityComparable = requires(T a, T b) {{a == b} -> bool; };void f(EqualityComparable&&); // 有制約函數(shù)模板的聲明 // 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{})); // 錯(cuò)誤:非 EqualityComparable }更多請(qǐng)參見:制約與概念 - cppreference.com
2.4 友元
與其使用printOn函數(shù)打印元素,不如重載operator<<,然而通常operator<<會(huì)實(shí)現(xiàn)為非成員函數(shù)。下面在類內(nèi)定義友元,它是一個(gè)普通函數(shù)
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;} };如果在類外定義友元,類模板參數(shù)不可見,事情會(huì)復(fù)雜很多
template<typename T> class Stack {...friend std::ostream& operator<<(std::ostream&, const Stack<T>); };std::ostream& operator<<(std::ostream& os,const Stack<T>& stack) // 錯(cuò)誤:類模板參數(shù)T不可見 {stack.printOn(os);return os; }有兩個(gè)解決方案:
-
一是隱式聲明一個(gè)新的函數(shù)模板,并使用不同的模板參數(shù)
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; } -
二是將友元前置聲明為模板,而友元參數(shù)中包含類模板,這樣就必須先前置聲明類模板
template<typename T> // operator<<中參數(shù)中要求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; }
同樣,函數(shù)只有被調(diào)用到時(shí)才實(shí)例化,元素沒有定義operator<<時(shí)也可以使用這個(gè)類,只有調(diào)用operator<<時(shí)才會(huì)出錯(cuò)
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'; // 錯(cuò)誤:元素類型不支持operator<<2.5 類模板特化
模板的實(shí)際應(yīng)用中,有一些概念和應(yīng)用很容易讓人混淆,現(xiàn)在就分析一下模板的特化和實(shí)例化。編寫模板的代碼,最終的目的是應(yīng)用,而在實(shí)際應(yīng)用的過程中,大家最經(jīng)常使用的是模板的實(shí)例化。也就是說模板說一族類或函數(shù)的抽象,那么要使用它,就需要把它應(yīng)用到某個(gè)具體的類或者函數(shù)上。
另外還有一個(gè)繞不開的就是:特化(specialization),從目前的教材來看有兩種理解:
- 凡是把模板用具體的值來替代的過程都叫特化。如果這么理解,實(shí)例化也是特化的一種。
- 特化是普通模板通過具體的值來替換后不能滿足一些特定的情況下的要求,需要對(duì)其進(jìn)行特別的處理,包括偏特化和全特化。
全特化寫法
template<> class Stack<std::string> {... };實(shí)例:
#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 }當(dāng)我們使用std::string作為模板參數(shù)時(shí),就會(huì)實(shí)例化這個(gè)用std::string特化的類.
我的理解:模板的特化就是為了處理一些非一般情況
2.6 偏特化
函數(shù)是沒有偏特化的。所以這里只是介紹類的偏特化。所謂偏特化,又叫局部特化或者部分特化,也就是在特定的條件下使用特定的對(duì)象來替換模板參數(shù),但又不能完全替換
#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*> {}我們定義了一個(gè)類模板
T仍然是模板參數(shù),但是為了T*特化
具有多個(gè)參數(shù)的部分特化
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*>如果有多個(gè)模板匹配,就會(huì)歧義:
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 默認(rèn)模板參數(shù)
對(duì)類模板,你可以設(shè)置一個(gè)默認(rèn)模板參數(shù),例如,對(duì)于Stack你可以設(shè)置一個(gè)默認(rèn)模板參數(shù)管理內(nèi)容
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 }這個(gè)例子就是使用std::vector作為默認(rèn)內(nèi)容管理器
注意:這個(gè)類現(xiàn)在有兩個(gè)模板參數(shù),所以每個(gè)成員函數(shù)也應(yīng)該有兩個(gè)模板參數(shù)
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 類型別名
為整個(gè)類型定義一個(gè)新名字讓類模板更方便使用
通過使用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>;這樣你就可以為整個(gè)類型定義一個(gè)別名,更方便使用
由于模板不是一個(gè)類型,所以不能定義一個(gè)typedef引用一個(gè)模板,但是新標(biāo)準(zhǔn)(since C++11)允許使用using為類模板定義一個(gè)別名
以Stack使用std::deque`管理元素為例
template<typename T> using DequeStack = Stack<T, std::deque<T>>;這樣我們就可以使用DequeStack<int> 代替 Stack<int,std::deque<int>>,這兩個(gè)表達(dá)的完全相同
2.9 類模板參數(shù)推斷
在C++17之前,你必須傳入所有的模板參數(shù)類型,但是,C++17之后,這個(gè)限制放松了,如果能通過構(gòu)造函數(shù)推斷出模板參數(shù)類型,你可以不用明確的指定參數(shù)類型
Stack< int> intStack1; // stack of strings Stack< int> intStack2 = intStack1; // OK in all versions Stack intStack3 = intStack1; // OK since C++17構(gòu)造函數(shù):
template<typename T> class Stack { private: std::vector<T> elems; // elementspublic: Stack () = default; Stack (T const& elem) // initialize stack with one element: elems({elem}) { }… };你可以這樣聲明一個(gè)Stack:
Stack intStack = 0; // Stack<int> deduced since C++17通過用整初始化Stack,推斷出模板參數(shù)T為int,從而實(shí)例化一個(gè)Stack<int>
原則上也可以傳遞字符串字面值常量,但這樣會(huì)造成許多麻煩。用引用傳遞模板類型T的實(shí)參時(shí),模板參數(shù)不會(huì)decay,最終得到的類型是原始數(shù)組類型
Stack stringStack = "bottom"; // Stack<char const[7]> deduced since C++17傳值的話則不會(huì)有這種問題,模板實(shí)參會(huì)decay,原始數(shù)組類型會(huì)轉(zhuǎn)換為指針
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傳值時(shí)最好使用std::move以避免不必要的拷貝
template<typename T> class Stack {public:Stack(T x) : v({std::move(x)}) {}private:std::vector<T> v; };**推斷指引(**Deduction Guides)
如果構(gòu)造函數(shù)不想使用傳值方式聲明,也有其他的解決辦法,
你可以定義一個(gè)專用的類型指引,將C字符串推斷為std::string
Stack( char const*) -> Stack<std::string>;這個(gè)指引必須出現(xiàn)在類定義的塊,或者命名空間里
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因?yàn)椴荒苁褂每截悩?gòu)造(=) 傳遞一個(gè)字符串去構(gòu)造一個(gè)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; };這樣可以為了參數(shù)化值而定義一個(gè)聚合,它可以像其他類模板一樣聲明對(duì)象,同時(shí)當(dāng)作一個(gè)聚合使用
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,初始化就無法進(jìn)行,因?yàn)锳沒有構(gòu)造函數(shù)來推斷。std::array也是一個(gè)聚合,元素類型和大小都是參數(shù)化的,C++17為其定義了一個(gè)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 }; // 等價(jià)于 std::array<int, 4> a{ 1, 2, 3, 4 };累死了,中秋還在肝…
參考解答: GotW #8: CHALLENGE EDITION: Exception Safety ??
C++17之后,如果參數(shù)類型可以從構(gòu)造函數(shù)推斷出來,可以跳過寫模板參數(shù)即 ??
總結(jié)
以上是生活随笔為你收集整理的类模板(参考《C++ Templates 英文版第二版》)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 收集20个经典的Java面试题
- 下一篇: C++入门