C++ 学习笔记
作者:readywang(王玉龍)
template 是 c++ 相當重要的組成部分,堪稱 c++語言的一大利器。在大大小小的 c++ 程序中,模板無處不在。c++ templates 作為模板學習的經典書籍,歷來被無數 c++學習者所推崇。第二版書籍覆蓋了 c++ 11 14 和 17 標準,值得程序猿們精讀學習,特此整理學習筆記,將每一部分自認為較為重要的部分逐條陳列,并對少數錯誤代碼進行修改
一、函數模板
1.1 函數模板初探
1.模板實例化時,模板實參必須支持模板中類型對應的所有運算符操作。
template?<typename?T> T?max(const?T?&a,?const?T?&b)?{return?a?>?b??a?:?b; }class?NdGreater?{ };int?main()?{NdGreater?n1,?n2;::max(n1,?n2);?//?不支持?>?編譯報錯 }2.模板編譯時會進行兩階段檢查
a.模板定義時,進行和類型參數無關的檢查,如未定義的符號等。
b.模板實例化時,進行類型參數相關的檢查。
template<typename?T> void?foo(T?t)?{undeclared();?//?如果?undeclared()未定義,第一階段就會報錯,因為與模板參數無關static_assert(sizeof(T)?>?10,?"T?too?small");?//與模板參數有關,只會在第二階段報錯 }3.根據兩階段檢查,模板在實例化時要看到完整定義,最簡單的方法是將實現放在頭文件中。
1.2 模板參數推斷
1.函數模板的模板參數可以通過傳遞的函數參數進行推斷。
2.函數推斷時會用到參數類型轉換,規則如下:
a.如果函數參數是按引用傳遞的,任何類型轉換都不被允許。(此處有疑問,const 轉換還是可以的)
b.如果函數參數是按值傳遞的,可以進行退化(decay)轉換:const(指針或者引用只有頂層 const 可以被忽略) 和 volatile 被忽略;引用變為非引用;數組和函數變為對應指針類型。
template?<typename?T> void?RefFunc(const?T?&a,?const?T?&b){};template?<typename?T> void?NoRefFunc(T?a,?T?b){};int?main()?{int?*const?ic?=?nullptr;const?int?*ci?=?nullptr;int?*p?=?nullptr;RefFunc(p,?ic);??//?ok?頂層const可以被忽略?T?為?int?*RefFunc(p,?ci);??//?error?底層const不可以忽略NoRefFunc(p,?ci);?//?error?底層const不可以忽略int?i?=?0;int?&ri?=?i;NoRefFunc(i,?ri);?//?ok?ri從int?&轉換為intint?arr[4];NoRefFunc(p,?arr);??//?ok?arr?被推斷為int?*NoRefFunc(4,?5.0);??//?error?T?可以推斷為int或double }3.上文的最后一句調用,類型推斷具有二義性,無法正確實例化。可以通過以下方式解決
a.類型轉換:
b.顯式指定模板實參:
NoRefFunc(static_cast<double>(4),?5.0);??//?ok?類型轉換NoRefFunc<int>(4,?5.0);??//?顯式指定4.函數模板無法通過默認參數推斷模板參數。如果函數模板只有一個函數參數,且函數參數提供了默認值的情況,應該為模板類型參數 T 也提供和函數參數默認值匹配的默認類型。
template?<typename?T> void?Default(T?t?=?0){};Default();?//?error?無法推斷為inttemplate?<typename?T?=?int> void?Default(T?t?=?0){};Default();?//?ok?默認類型為int1.3 多模板參數
1.當函數返回類型不能或不便由函數參數類型直接推斷時,可以在函數模版中新增模板參賽指定返回類型。
2.c++11 之后,可以通過 auto + decltype +尾后返回類型 推斷函數模板返回類型。當函數參數為引用類型時,返回類型應該為非引用。而decltype 會保留引用,因此還需通過 decay 進行類型退化。
3.c++14 之后,可以通過 auto 直接推斷函數模板返回類型,前提是函數內部的多個返回語句推斷出的返回類型要一致。auto 會自動對類型進行 decay。
4.c++11 之后,可以通過 common_type 返回多個模版類型參賽的公共類型,common_type 返回的類型也是 decay 的。
#include<type_traits> //?單獨通過RT指定返回類型 template?<typename?RT,?typename?T1,?typename?T2> RT?max1(const?T1&?a,?const?T2&?b)?{?return?a?>?b???a?:?b;?}//?auto?c++11支持?通過decay?進行類型退化?typename?用于聲明嵌套從屬名稱?type?為類型而不是成員 template?<typename?T1,?typename?T2> auto?max2(const?T1&?a,?const?T2&?b)?->?typename?std::decay<decltype(a?>?b???a?:?b)>::type?{?return?a?>?b???a?:?b;?}//?auto?c++14支持 template?<typename?T1,?typename?T2> auto?max3(const?T1&?a,?const?T2&?b)?{?return?a?>?b???a?:?b;?}//?common_type?c++11支持?max4(5,?7.3)?max4(7.4,?5)?的返回類型均被推斷為double template?<typename?T1,?typename?T2> typename?std::common_type<T1,?T2>::type?max4(const?T1&?a,?const?T2&?b)?{?return?a?>?b???a?:?b;?}1.4 默認模板參數
1.可以給模板參數指定默認值。
//?默認模板參賽?因為RT需要T1?T2推斷,所以放在最后 template?<typename?T1,?typename?T2,?typename?RT?=?typename?std::common_type<T1,?T2>::type> RT?max5(const?T1&?a,?const?T2&?b)?{?return?a?>?b???a?:?b;?}1.5 函數模板重載
1.一個非模板函數可以和同名的函數模板共存,并且函數模板可實例化為和非模板函數具有相同類型參數的函數。函數調用時,若匹配度相同,將優先調用非模板函數。但若顯式指定模板列表,則優先調用函數模板。
2.函數模板不可以進行類型自動轉換,非模板函數可以。
#pragma?oncetemplate?<typename?T> T?max(T?a,?T?b)?{return?a?>?b???a?:?b; }template?<typename?RT,?typename?T1,?typename?T2> RT?max(T1?a,?T2?b)?{return?a?>?b???a?:?b; } int?max(int?a,?int?b)?{return?a?>?b???a?:?b; }int?main()?{::max(6,?8);??//?調用非模板函數::max<>(6,?8);?//?調用函數模板?max<int>::max('a',?'b');?//?調用函數模板?max<char>::max(4,?4.0);?//?通過類型轉換調用非模板函數::max<double>(4,?4.0);?//指定了返回類型?調用max<double,int,double> }3.調用函數模板時,必須保證函數模板已經定義。
int?max(int?a,?int?b)?{return?a?>?b???a?:?b; }template?<typename?T> T?max(T?a,?T?b,?T?c)?{return?max(max(a,b),c);??//T為int時,并不會調用max<int>?而是調用非模板函數 }template?<typename?T> T?max(T?a,?T?b)?{return?a?>?b???a?:?b; }max(1,?2,?3);??//?最終調用非模板函數比較 max("sjx",?"wyl",?"shh");?//?error?找不到二元的max<const?char?*>二、類模板
2.1 stack 類模板實現
1.類模板不可以定義在函數作用域或者塊作用域內部,通常定義在 global/namespace/類作用域。
#include<vector> #include<iostream>template?<typename?T> class?Stack { public:void?push(const?T&?value);void?pop();T?top();int?size()?const?{?elem_.size();?};bool?empty()?const?{?return?elem_.empty();?};void?print(std::ostream?&?out)?const; protected:std::vector<T>?elem_; };template?<typename?T> void?Stack<T>::push(const?T?&value) {elem_.push_back(value); }template?<typename?T> void?Stack<T>::pop() {elem_.pop_back(); }template?<typename?T> T?Stack<T>::top() {return?elem_.back(); }template?<typename?T> void?Stack<T>::print(std::ostream?&out)?const {for?(auto?e?:?elem_){out?<<?e?<<?std::endl;} }2.2 stack 類模板使用
1.直到 c++17,使用類模板都需要顯式指定模板參數。
2.類模板的成員函數只有在調用的時候才會實例化。
2.3 部分使用類模板
1.類模板實例化時,模板實參只需要支持被實例化部分所有用到的操作。
int?main() {//?只會實例化類模板中的push?和?print函數Stack<int>?s;s.push(3);s.print(std::cout);//?Stack<int>未重載<<運算符,實例化print函數時失敗Stack<Stack<int>>?ss;ss.push(s);ss.print(std::cout);return?0; }2.c++11 開始,可以通過 static_assert 和 type_traits 做一些簡單的類型檢查
template?<typename?T> class?C {static_assert(std::is_default_constructible<T>::value,?"class?C?requires?default?contructible"); };2.4 友元
2.5 模板特化
1.可以對類模板的一個參數進行特化,類模板特化的同時需要特化所有的成員函數,非特化的函數在特化后的模板中屬于未定義函數,無法使用。
//?stringle類型特化 template?<> class?Stack<std::string> { public:void?push(const?std::string&?value);/*?特化其他成員函數*/ };2.6 模板偏特化
1.類模板特化時,可以只特化部分參數,或者對參數進行部分特化。
//?指針類型特化 template?<typename?T> class?Stack<T?*> {public:void?push(T?*value);void?pop();T*?top();int?size()?const?{?elem_.size();?};bool?empty()?const?{?return?elem_.empty();?}; protected:std::vector<T?*>?elem_; };template?<typename?T> void?Stack<T*>::push(T?*value) {elem_.push_back(value); }template?<typename?T> void?Stack<T*>::pop() {elem_.pop_back(); }template?<typename?T> T*?Stack<T*>::top() {return?elem_.back(); }2.7 默認類模板參數
1.類模板也可以指定默認模板參數。
template?<typename?T,?typename?COND?=?std::vector<T>?> class?Stack { public:void?push(const?T&?value);void?pop();T?top();int?size()?const?{?elem_.size();?};bool?empty()?const?{?return?elem_.empty();?}; protected:COND?elem_; };template?<typename?T,?typename?COND> void?Stack<T,?COND>::push(const?T?&value) {printf("template?1\n");elem_.push_back(value); }template?<typename?T,?typename?COND> void?Stack<T,?COND>::pop() {elem_.pop_back(); }template?<typename?T,?typename?COND> T?Stack<T,?COND>::top() {return?elem_.back(); }2.8 類型別名
1.為了便于使用,可以給類模板定義別名。
typedef?Stack<int>?IntStack; using?DoubleStack?=?Stack<double>;2.c++11 開始可以定義別名模板,為一組類型取一個方便的名字。
template?<typename?T> using?DequeStack?=?Stack<T,?std::deque<T>>;3.c++14 開始,標準庫使用別名模板技術,為所有返回一個類型的 type_trait 定義了快捷的使用方式。
//?stl庫定義 namespace?std {template?<typename?T>using?add_const_t?=?typename?add_const<T>::type; }typename?add_const<T>::type;??//c++?11?使用 std::add_const_t<T>;?//c++14使用2.9 類模板類型推導
1.c++17 開始,如果構造函數能夠推斷出所有模板參數的類型,那么不需要指定參數類型了。
template?<typename?T> class?Stack { public:Stack()?=?default;Stack(T?e):?elem_({e}){}; protected:std::vector<T>?elem_; };Stack?intStack?=?0;?//通過構造函數推斷為int2.類型推導時,構造函數參數應該按照值傳遞,而非按引用。引用傳遞會導致類型推斷時無法進行 decay 轉化。
Stack?strStack?=?"sjx"; //若構造函數參數為值傳遞,則T為const?char?*,引用傳遞時則為const?char[4]3.c++ 17 支持提供推斷指引來提供額外的推斷規則,推斷指引一般緊跟類模板定義之后。
//?推斷指引,傳遞字符串常量時會被推斷為string Stack<const?char?*>?->?Stack<std::string>2.10 聚合類的模板化
1.聚合類:沒有顯式定義或繼承來的構造函數,沒有非 public 的非靜態成員,沒有虛函數,沒有 virtual,private ,protected 繼承。聚合類也可以是模板。
template?<typename?T> struct?ValueWithComment {T?val;std::string?comment; };ValueWithComment<int>?vc; vc.val?=?42; vc.comment?=?"sjx";三、非類型模板參數
3.1 類模板的非類型模板參數
1.模板參數不一定是類型,可以是數值,如可以給 Stack 指定最大容量,避免使用過程元素增刪時的內存調整。
template?<typename?T,?int?MAXSIZE> class?Stack { public:Stack():num_(0){};void?push(const?T&?value);void?pop();T?top();int?size()?const?{?return?num_;?};bool?empty()?const?{?return?num_?==?0;?}; protected:T?elem_[MAXSIZE];int?num_; };template?<typename?T,?int?MAXSIZE> void?Stack<T,?MAXSIZE>::push(const?T?&value) {printf("template?1\n");assert(num_?<?MAXSIZE);elem_[num_++]?=?value; }template?<typename?T,?int?MAXSIZE> void?Stack<T,?MAXSIZE>::pop() {assert(num_?>?0);--num_; }template?<typename?T,?int?MAXSIZE> T?Stack<T,?MAXSIZE>::top() {assert(num_?>?0);return?elem_[0]; }3.2 函數模板的非類型模板參數
1.函數模板也可以指定非類型模板參數。
template<typename?T,?int?VAL> T?addval(const?T?&num) {return?num?+?VAL; }int?main() {std::vector<int>?nums?=?{1,?2,?3,?4,?5};std::vector<int>nums2(nums.size(),0);std::transform(nums.begin(),nums.end(),nums2.begin(),addval<int,5>);for(auto?num?:?nums2){printf("%d\n",num);} }3.3 非類型模板參數限制
1.非類型模板參數只能是整形常量(包含枚舉),指向 objects/functions/members 的指針,objects 或者 functions 的左值引用,或者是 std::nullptr_t(類型是 nullptr),浮點數和類對象不能作為非類型模板參數。
2.當傳遞對象的指針或者引用作為模板參數時,對象不能是字符串常量,臨時變量或者數據成員以及其它子對象。
3.對于非類型模板參數是 const char*的情況,不同 c++版本有不同限制
a. C++11 中,字符串常量對象必須要有外部鏈接
b. C++14 中,字符串常量對象必須要有外部鏈接或內部鏈接
c. C++17 中, 無鏈接屬性也可
4.內部鏈接:如果一個名稱對編譯單元(.cpp)來說是局部的,在鏈接的時候其他的編譯單元無法鏈接到它且不會與其它編譯單元(.cpp)中的同樣的名稱相沖突。例如 static 函數,inline 函數等。
如果一個名稱對編譯單元(.cpp)來說不是局部的,而在鏈接的時候其他的編譯單元可以訪問它,也就是說它可以和別的編譯單元交互。
6.非類型模板參數的實參可以是任何編譯器表達式,不過如果在表達式中使用了 operator >,就必須將相應表達式放在括號里面,否則>會被作為模板參數列表末尾的>,從而截斷了參數列表。
#include<string>template?<?double?VAL?>?//?error?浮點數不能作為非類型模板參數 double?process(double?v) {return?v?*?VAL; }template?<?std::string?name?>?//?error?class對象不能作為非類型模板參數 class?MyClass?{ };template?<?const?char?*?name?> class?Test{ };extern?const?char?s01[]?=?"sjx";?//?外部鏈接 const?char?s02[]?=?"sjx";?//?內部鏈接template<int?I,?bool?B> class?C{}; int?main() {Test<"sjx">?t0;?//?error?before?c++17Test<s01>?t1;?//?okTest<s02>?t2;?//?since?c++14static?const?char?s03[]?=?"sjx";?//?無鏈接Test<s03>?t3;?//?since?c++17C<42,?sizeof(int)>?4?>?c;?//error?第一個>被認為模板參數列表已經結束C<42,?(sizeof(int)>?4)?>?c;?//?ok }3.4 用 auto 作為非模板類型參數的類
1.從 C++17 開始,可以不指定非類型模板參數的具體類型(代之以 auto),從而使其可以用于任意有效的非類型模板參數的類型。
template?<typename?T,?auto?MAXSIZE> class?Stack { public:using?size_type?=?decltype(MAXSIZE);?//?根據MAXSIZE推斷類型 public:Stack():num_(0){};void?push(const?T&?value);void?pop();T?top();size_type?size()?const?{?num_;?};bool?empty()?const?{?num_?==?0;?}; protected:T?elem_[MAXSIZE];size_type?num_; };template?<typename?T,?int?MAXSIZE> void?Stack<T,?MAXSIZE>::push(const?T?&value) {printf("template?1\n");assert(num_?<?MAXSIZE);elem_[num_++]?=?value; }template?<typename?T,?int?MAXSIZE> void?Stack<T,?MAXSIZE>::pop() {assert(num_?>?0);--num_; }template?<typename?T,?int?MAXSIZE> T?Stack<T,?MAXSIZE>::top() {assert(num_?>?0);return?elem_[0]; }int?main() {Stack<int,20u>?s1;?//?size_type為unsigned?intStack<double,40>?s2;?//?size_type為int }四、可變參數模板
4.1 可變參數模板
1.c++11 開始,模板可以接收一組數量可變的參數。
#include?<iostream> void?print(){};??//?遞歸基 template<typename?T,?typename?...Types> void?print(T?firstArg,?Types?...?args) {std::cout?<<?firstArg?<<?std::endl;print(args...); }int?main(){print(7.5,?"hello",?10);?//?調用三次模板函數后再調用普通函數 }當兩個函數模板的區別只在于尾部的參數包的時候,會優先選擇沒有尾部參數包的函數模板。
3.c++11 提供了sizeof...運算符來統計可變參數包中的參數數目
template<typename?T,?typename?...Types> void?print(T?firstArg,?Types?...?args) {std::cout?<<?sizeof...(Types)?<<?std::endl;std::cout?<<?sizeof...(args)?<<?std::endl;std::cout?<<?firstArg?<<?std::endl;print(args...); }4.函數模板實例化時會將可能調用的函數都實例化。
#include?<iostream> template<typename?T,?typename?...Types> void?print(T?firstArg,?Types?...?args) {std::cout?<<?firstArg?<<?std::endl;if(sizeof...(args)?>?0)print(args...);?// error 缺少遞歸基。即使只在參數包數目>0時調用,args為0個參數時的print函數 } int?main(){print(7.5,?"hello",?10);?//?調用三次模板函數后再調用普通函數 }4.2 折疊表達式
1.C++17 提供了一種可以用來計算參數包(可以有初始值)中所有參數運算結果的二元運算符 ...
template<typename?...T> auto?sum(T?...s) {return?(...?+?s);?//?((s1+s2)+s3)... }
4.3 可變參數模板的使用
4.4 變參類模板和變參表達式
可變參數包可以出現在數學表達式中,用于表達式運算。
可變參數包可以出現在下標中,用于訪問指定下標的元素。
可變參數包可以為非類型模板參數。
可變參數包也可以作為類模板參數,用于表示數據成員的類型,或對象可能的類型等含義。
5.推斷指引也可以是可變參數的。
namespace?std?{//?std::array?a{42,43,44}?會被推斷為?std::array<int,3>?a{42,43,44}template<typename?T,?typename…?U>?array(T,?U…)?->?array<enable_if_t<(is_same_v<T,?U>?&&?…),?T>,?(1?+?sizeof…(U))>; }五、基礎技巧
5.1 typename 關鍵字
1.c++規定模板中通過域作用符訪問的嵌套從屬名稱不是類型名稱,為了表明該名稱是類型,需要加上 typename 關鍵字。
template<typename?T> class?MyClass { public:void?foo(){/*?若無typename?SubType會被當做T中的一個static成員或枚舉值下方的表達式被理解為SubType?和?ptr的乘積?*/typename?T::SubType?*ptr;//...} };5.2 零初始化
1.c++中對于未定義默認構造函數的類型對象,定義時一般不會進行默認初始化,這時候對象的值將是未定義的。
2.在模板中定義對象時,為了避免產生未定義的行為,可以進行零初始化。
template<typename?T> void?foo() {T?x?=?T();?//?對x提供默認值 }5.3 使用 this ->
1.若類模板的基類也是類模板,這時在類模板中不能直接通過名稱調用從基類繼承的成員,而應該通過 this-> 或 Base::。
template<typename?T> class?Base { public:void?bar(){}; };template<typename?T> class?Derived:public?Base<T> { public:void?foo(){bar();?//ERROR?無法訪問基類bar成員,若global存在bar函數,則會訪問全局bar函數this->bar();?//?okBase<T>::bar();?//?ok} };5.4 使用裸數組或字符串常量的模板
1.當向模板傳遞裸數組或字符串常量時,如果是引用傳遞,則類型不會 decay;如果是值傳遞,則會 decay。
2.也可以通過將數組或字符串長度作為非類型模板參數,定義可以適配不同長度的裸數組或字符串常量的模板。
template?<typename?T> void?foo(T?t){};template?<typename?T> void?RefFoo(const?T?&t){};template<typename?T,?int?N,?int?M> bool?less?(T(&a)[N],?T(&b)[M]) {for?(int?i?=?0;?i<N?&&?i<M;?++i){if?(a[i]<b[i])?return?true;?if?(b[i]<a[i])?return?false;}return?N?<?M; }int?main(){foo("hello");?//?T?為const?char?*RefFoo("hello");?//?T?為const?char[6]less("sjx",?"wyl");?//?T?為const?char,?N,M為3 }5.5 成員模板
1.不管類是普通類還是類模板,類中的成員函數都可以定義為模板。
2.若是將構造函數或者賦值運算符定義為模板函數,此時定義的模板函數不會取代默認的的構造函數和賦值運算符。下面定義的 operater = 只能用于不同類型的 stack 之間的賦值,若是相同類型,仍然采用默認的賦值運算符。
#include<deque>template?<typename?T,?typename?COND?=?std::deque<T>?> class?Stack { public:void?push(const?T&?value);void?pop();T?top();int?size()?const?{?elem_.size();?};bool?empty()?const?{?return?elem_.empty();?};//實現不同類型的stack之間的相互賦值template?<typename?T2>Stack<T,COND>?&?operator?=?(const?Stack<T2>&?s); protected:COND?elem_; };template?<typename?T,?typename?COND> template?<typename?T2> Stack<T,COND>?&?Stack<T,COND>::operator?=?(const?Stack<T2>&?s) {if((void?*)this?==?(void?*)&s){return?*this;}Stack<T2>?tmp(s);elem_.clear();while(!tmp.empty()){elem_.push_front(tmp.top());tmp.pop();}return?*this; }2.成員函數模板也可以被偏特化或者全特化。
#include<string>class?BoolString?{ private:std::string?value; public:BoolString?(std::string?const&?s):?value(s)?{}template<typename?T?=?std::string>T?get()?const?{return?value;} };//?進行全特化,?全特化版本的成員函數相當于普通函數, //?放在頭文件中會導致重復定義,因此必須加inline template<> inline?bool?BoolString::get<bool>()?const {return?value?==?"true"?||?value?==?"1"; }int?main() {BoolString?s1("hello");s1.get();??//?"hello"s1.get<bool>();?//?false }3.當通過依賴于模板參數的對象通過.調用函數模板時,需要通過 template 關鍵字提示<是成員函數模板參數列表的開始,而非<運算符。
#include<bitset> #include<iostream>template<unsigned?int?N> void?printBitSet(const?std::bitset<N>?&bs) {//?bs依賴于模板參數N?此時為了表明to_string后是模板參數,需要加templatestd::cout?<<?bs.template?to_string<char,std::char_traits<char>,std::allocator<char>?>()?<<?std::endl; }4.c++14 引入的泛型 lambda 是對成員函數模板的簡化。
[](auto?x,?auto?y) {return?x?+?y; }//?編譯將以上lamba轉化為下方類 class?SomeCompilerSpecificName?{ public:SomeCompilerSpecificName();template<typename?T1,?typename?T2>auto?operator()?(T1?x,?T2?y)?const?{return?x?+?y;} };5.6 變量模板
1.c++14 開始,可以通過變量模板對變量進行參數化。
2.變量模板的常見應用場景是定義代表類模板成員的變量模板。
3.c++17 開始,標準庫用變量模板為其用來產生一個值(布爾型)的類型萃取定義了簡化方式。
#include<iostream>template?<typename?T?=?double> constexpr?T?pi{3.1415926};std::cout<<?pi<>?<<std::endl;?//?<>不可少,輸出3.1415926 std::cout<<?pi<int>?<<std::endl;?//?輸出3template<typename?T> class?MyClass?{ public:static?constexpr?int?max?=?1000;?//?類靜態成員 };//?定義變量模板表示類靜態成員 template?<typename?T> int?myMax?=?MyClass<T>::max;//?使用更方便 auto?i?=?myMax<int>;?//?相當于auto?i?=?MyClass<int>::max;//?萃取簡化方式,since?c++17 namespace?std?{template<typename?T>constexpr?bool?is_const_v?=?is_const<T>::value; }5.7 模板模板參數
1.當非類型模板參數是一個模板時,我們稱它為模板模板參數。
2.實例化時,模板模板參數和實參的模板參數必須完全匹配。
#include?<deque> #include?<vector>//?錯誤定義 deque 中的模板參數有兩個:類型和默認參數allocator //?而模板模板參數Cont的參數只有類型Elem template<typename?T, template<typename?Elem>?class?Cont?=?std::deque> class?Stack?{ private:Cont<T>?elems;?//?elements public:void?push(T?const&);?//?push?elementvoid?pop();?//?pop?elementT?const&?top()?const;?//?return?top?elementbool?empty()?const?{?//?return?whether?the?stack?is?emptyreturn?elems.empty();} };//?正確定義 template<typename?T,?template<typename?Elem,?typename?= std::allocator<Elem>>?class?Cont?=?std::deque> class?Stack?{ private:Cont<T>?elems;?//?elements public:void?push(T?const&);?//?push?elementvoid?pop();?//?pop?elementT?const&?top()?const;?//?return?top?elementbool?empty()?const?{?//?return?whether?the?stack?is?emptyreturn?elems.empty();} };//?使用 Stack<int>?iStack; Stack<double,std::vector>?dStack;六、移動語義和 enable_if<>
6.1 完美轉發
1.c++11 引入了引用折疊:創建一個引用的引用,引用就會折疊。除了右值引用的右值引用折疊之后還是右值引用之外,其它的引用全部折疊成左值引用。
2.基于引用折疊和 std::forward,可以實現完美轉發:將傳入將被參數的基本特性(是否 const,左值、右值引用)轉發出去。
#include?<utility> #include?<iostream> class?X?{ }; void?g?(X&)?{std::cout?<<?"g()?for?variable\n"; } void?g?(X?const&)?{std::cout?<<?"g()?for?constant\n"; } void?g?(X&&)?{std::cout?<<?"g()?for?movable?object\n"; }template<typename?T> void?f?(T&&?val)?{g(std::forward<T>(val)); } int?main() {X?v;?//?create?variableX?const?c;?//?create?constantf(v);?//?f()?for?variable?calls?f(X&)?=>?calls?g(X&)f(c);?//?f()?for?constant?calls?f(X?const&)?=>?calls?g(X?const&)f(X());?//?f()?for?temporary?calls?f(X&&)?=>?calls?g(X&&)f(std::move(v));?//?f()?for?move-enabled?variable?calls?f(X&&)=>calls?g(X&&) }6.2 特殊成員函數模板
1.當類中定義了模板構造函數時:
a.定義的模板構造函數不會屏蔽默認構造函數。
b.優先選用匹配程度高的構造函數
c.匹配程度相同時,優先選用非模板構造函數
#include?<utility> #include?<string> #include?<iostream> class?Person { private:std::string?name; public://?generic?constructor?for?passed?initial?name:template<typename?STR>explicit?Person(STR&&?n)?:?name(std::forward<STR>(n))?{std::cout?<<?"TMPL-CONSTR?for?’"?<<?name?<<std::endl;}//?copy?and?move?constructor:Person?(Person?const&?p)?:?name(p.name)?{std::cout?<<?"COPY-CONSTR?Person?’"?<<?name?<<?"’\n";}Person?(Person&&?p)?:?name(std::move(p.name))?{std::cout?<<?"MOVE-CONSTR?Person?’"?<<?name?<<?"’\n";} };int?main() {Person?p1("tmp");?//?ok?調用模板函數n?被推斷為const?char?[4]類型,可以賦值給stringPerson?p2(p1);?//?error?模板函數相比Person?(Person?const&?p)構造函數更匹配,n?被推斷為Person?&類型,最終將Person對象賦值給stringPerson?p3(std::move(p1));?//?ok?同等匹配程度時,優先調用Person?(Person&&?p) }6.3 通過 std::enable_if 禁用模板
1.c++11 提供了輔助模板 std::enable_if,使用規則如下:
a.第一個參數是布爾表達式,第二個參數為類型。若表達式結果為 true,則 type 成員返回類型參數,此時若未提供第二參數,默認返回 void。
b.若表達式結果為 false,根據替換失敗并非錯誤的原則,包含 std::enable_if 的模板將會被忽略。
2.c++14 提供了別名模板技術(見 2.8 節),可以用 std::enable_if_t<>代替 std::enable_if<>::type.
3.若不想在聲明中使用 std::enable_if,可以提供一個額外的、有默認值的模板參數。
#include?<type_traits> template<typename?T> typename?std::enable_if<(sizeof(T)?>?4)>::type foo1(){}//?std::enable_if為true時等同下方函數,未提供第二參數默認返回void;?false時函數模板被忽略 //?void?foo1(){}; template<typename?T> typename?std::enable_if<(sizeof(T)?>?4),?bool>::type foo2(){}//?std::enable_if為true時等同下方函數,false時函數模板被忽略 //?bool?foo2(){};//?提供額外默認參數進行類型檢查 template<typename?T,?typename?=?typename?std::enable_if<(sizeof(T)?>?4)>::type>void?foo3(){}//?std::enable_if為true時等同下方函數模板,false時函數模板被忽略 //?template<typename?T,?typename?=?void> //?void?foo3(){};6.4 使用 std::enable_if
1.通過 std::enable_if 和標準庫的類型萃取 std::is_convertiable<FROM, TO>可以解決 6.2 節構造函數模板的問題。
class?Person { private:std::string?name; public://?只有STR可以轉換為string時才有效template<typename?STR,?typename?=?typename?std::enable_if<std::is_convertible<STR,?std::string>::value>::type>explicit?Person(STR&&?n)?:?name(std::forward<STR>(n))?{std::cout?<<?"TMPL-CONSTR?for?’"?<<?name?<<std::endl;}//?copy?and?move?constructor:Person?(Person?const&?p)?:?name(p.name)?{std::cout?<<?"COPY-CONSTR?Person?’"?<<?name?<<?"’\n";}Person?(Person&&?p)?:?name(std::move(p.name))?{std::cout?<<?"MOVE-CONSTR?Person?’"?<<?name?<<?"’\n";} };6.5 使用 concept 簡化 enable_if<>表達式
1.c++20 提出了 concept 模板可以進行編譯期條件檢查,大大簡化了 enable_if
template?<typename?T> concept?convert_to_string?=?std::is_convertible<STR,?std::string>::value;class?Person { private:std::string?name; public://?只有STR可以轉換為string時才有效template<convert_to_string?STR>explicit?Person(STR&&?n)?:?name(std::forward<STR>(n))?{std::cout?<<?"TMPL-CONSTR?for?’"?<<?name?<<std::endl;}//?copy?and?move?constructor:Person?(Person?const&?p)?:?name(p.name)?{std::cout?<<?"COPY-CONSTR?Person?’"?<<?name?<<?"’\n";}Person?(Person&&?p)?:?name(std::move(p.name))?{std::cout?<<?"MOVE-CONSTR?Person?’"?<<?name?<<?"’\n";} };七、按值傳遞還是按引用傳遞
7.1 按值傳遞
1.當函數參數按值傳遞時,原則上所有參數都會被拷貝。
2.當傳遞的參數是純右值時,編譯器會優化,避免拷貝產生;從 c++17 開始,要求此項優化必須執行。
3.按值傳遞時,參數會發生 decay,如裸數組退化為指針,const、volatile 等限制符會被刪除。
#include?<string> template?<typename?T> void?foo(T?t){};int?main(){std::string?s("Sjx");foo(s);?//?進行拷貝foo(std::string("sjx"));?//?避免拷貝foo("sjx");?//?T?decay為const?char?* }7.2 按引用傳遞
1.當函數參數按照引用傳遞時,不會被拷貝,并且不會 decay。
2.當函數參數按照引用傳遞時,一般是定義為 const 引用,此時推斷出的類型 T 中無 const。
3.當參數定義為非常量引用時,說明函數內部可以修改傳入參數,此時不允許將臨時變量傳給左值引用。此時若傳入參數是 const 的,則類型 T 被推斷為 const,此時函數內部的參數修改將報錯.
template?<typename?T> void?print(const?T?&t){};template?<typename?T> void?out(T?&t){};template?<typename?T> void?modify(T?&t) {t?=?T(); }; int?main(){print("sjx");?//?arg為const?char[3],?T?為char[3]std::string?s("Sjx");out(s);?//?okout(std::string("sjx"));?//?error?不能將臨時變量傳給左值引用const?std::string?name?=?"sjx";modify(name);?//error?T?被推斷為?const?std::string,?此時modify不能修改傳入參數 }4.對于給非 const 引用參數傳遞 const 對象導致編譯失敗的情形,可以通過 static_assert std::enable_if 或者 concept 等方式進行檢查。
//?static_assert?觸發編譯期錯誤 template?<typename?T> void?modify(T?&t) {static_assert(!std::is_const<T>::value,?"param?is?const");t?=?T(); };//?std::enable_if禁用模板 template?<typename?T,?typename?=?typename?std::enable_if<!std::is_const<T>::value>?> void?modify(T?&t) {t?=?T(); };//?concept禁用模板 template?<typename?T> concept?is_not_const?=?!std::is_const<T>::value;template?<is_not_const?T> void?modify(T?&t) {t?=?T(); };5.當完美轉發時,會將函數參數定義為右值引用。此時若在函數內部用 T 定義未初始化的變量,會編譯失敗。
template<typename?T> void?passR(T?&&t) {T?x; }std::string?s("Sjx"); passR(s);?//?error?T?推斷為string?&,初始化時必須綁定到對象7.3 使用 std::ref()和 std::cref()
1.c++11 開始,若模板參數定義為按值傳遞時,調用者可以通過 std::cref 或 std::ref 將參數按照引用傳遞進去。
2.std::cref 或 std::ref創建了一個 std::reference_wrapper<>的對象,該對象引用了原始參數,并被按值傳遞給了函數模板。std::reference_wrapper<>對象只支持一個操作:向原始對象的隱式類型轉換。
template<typename?T> void?foo(T?arg) {T?x; }int?main(){std::string?s?=?"hello";foo(s);?//?T?為std::stringfoo(std::cref(s));?//?error?T?為std::reference_wrapper<std::string>?只支持向std::string的類型轉換 }7.4 處理字符串常量和裸數組
1.字符串常量或裸數組傳遞給模板時,如果是按值傳遞,則會 decay;如果是按照引用傳遞,則不會 decay。實際應用時,可以根據函數作用加以選擇,若要比較大小,一般是按照引用傳遞;若是比較參數類型是否相同,則可以是按值傳遞。
7.5 處理返回值
1.函數返回值也可以是按值返回或按引用返回。若返回類型為非常量引用,則表示可以修改返回對象引用的對象。
2.模板中即使使用 T 作為返回類型,也不一定能保證是按值返回。
template<typename?T> T?retR(T?&&p) {return?T(); }template<typename?T> T?retV(T?t) {return?T(); }int?main() {int?x;retR(x);?//?error?T?被推斷為int?&retV<int?&>(x);?//?error?T推斷為int?& }3.為保證按值返回,可以使用 std::remove_reference<>或 std::decay<>。
7.6 關于模板參數聲明的推薦方法
1.一般通常按值傳遞,如有特殊需要,可以結合實際按引用傳遞。
2.定義的函數模板要明確使用范圍,不要過分泛化。
八、編譯期編程
8.1 模板元編程
1.模板元編程:在編譯期通過模板實例化的過程計算程序結果。
/*?定義用于編譯期判斷素數的模板?*/ template<unsigned?p?,unsigned?d> struct?DoIsPrime {//?從p%d?!=0?開始依次判斷?p%(d-1)!=0,p%(d-2)!=0,...static?constexpr?bool?value?=?(p?%?d?!=?0)?&&?DoIsPrime<p,?d-1>::value; };//?遞歸基 template<unsigned?p> struct?DoIsPrime<p?,2> {static?constexpr?bool?value?=?(p?%?2?!=?0); };//?提供給用戶調用的模板 template<unsigned?p> struct?IsPrime {static?constexpr?bool?value?=?DoIsPrime<p,?p?/?2>::value; };//?對于p/2<2的情形進行特化 template<> struct?IsPrime<0>?{static?constexpr?bool?value?=?false; };template<> struct?IsPrime<1>?{static?constexpr?bool?value?=?false; };template<> struct?IsPrime<2>?{static?constexpr?bool?value?=?false; };template<> struct?IsPrime<3>?{static?constexpr?bool?value?=?false; };int?main(){IsPrime<9>::value;?//?編譯期通過層層遞歸實例化最后得到結果為false }8.2 通過 constexpr 進行計算
1.c++11 提出了 constexpr 關鍵字可以用于修飾函數返回值,此時該函數為常量表達式函數,編譯器可以在編譯期完成該函數的計算。
2.c++11 中規定常量表達式函數在使用前必須要知道完整定義,不能僅僅是聲明,同時函數內部只能有一條返回語句。
3.c++14 中移除了常量表達式函數只能有一條返回語句的限制。
4.編譯器可以在編譯期完成該函數計算,但是是否進行還取決于上下文環境及編譯器。
//?constexpr函數只能有一個return語句 constexpr?bool?doIsPrime(unsigned?p,?unsigned?d) {return?d?!=?2???(p?%?d?!=?0)?&&?doIsPrime(p,?d?-?1)?:?(p?%?2?!=?0); }constexpr?bool?isPrime(unsigned?p) {return?p?<?4???!(p?<?2)?:?doIsPrime(p,?p?/?2); } int?main(){constexpr?bool?ret?=?isPrime(9);?????//?constexpr?限定必須在編譯期得到結果 }8.3 通過模板偏特化進行路徑選擇
1.可以通過模板偏特化在不同的實現方案之間做選擇。
//?根據是否是素數判斷是否喜歡 template?<unsigned?int?SZ,?bool?=?isPrime(SZ)> struct?IsLove;//?特化兩個版本處理 template<?unsigned?int?SZ> struct?IsLove<SZ,?false> {static?constexpr?bool?isLove?=?false; };template<?unsigned?int?SZ> struct?IsLove<SZ,?true> {static?constexpr?bool?isLove?=?true; }; int?main() {IsLove<520>::isLove;?//?編譯期就知道了520不是喜歡 }8.4 SFINAE(替換失敗不是錯誤)
SFINAE:當函數調用的備選方案中出現函數模板時,編譯器根據函數參數確定(替換)函數模板的參數類型及返回類型,最后評估替換后函數的匹配程度。替換過程中可能失敗,此時編譯器會忽略掉這一替換結果。
替換和實例化不同,替換只涉及函數函數模板的參數類型及返回類型,最后編譯器選擇匹配程度最高的函數模板進行實例化。
我們可以通過 SFINAE 原理在一些情形下忽略掉該模板。一種簡單用法是 6.3 6.4 中的 std::enable_if;對于上例,則可以采用 auto + 尾后返回的方式,使得在替換時就知道 T 中必須含有 size 成員的限制。
8.5 編譯期 if
除了前面介紹的忽略模板的方法,c++17 還提供了編譯期的條件判斷語法 if constexpr(...)。
九、實踐中使用模板
9.1 包含模式
模板在編譯期會進行實例化,實例化時需要提供模板的定義,所以對于模板相關代碼,正確用法是將聲明和定義均置于頭文件中。
9.2 模板和 inline
函數模板全特化后和普通函數相同,但函數模板一般定義在頭文件中,為了避免在多個模塊 include 時出現重復定義的錯誤,一般將全特化后的函數模板定義為 inline。
9.3 預編譯頭文件
預編譯頭文件不在 c++標準要求中,具體由編譯器實現。
預編譯頭文件:如果多個代碼文件的前 n 行均相同,編譯器就可以先對前 n 行進行編譯,再依次對每個文件從 n+1 行進行編譯。
為了充分利用預編譯頭文件功能,實踐中建議文件中#include 的順序盡量相同。
9.4 破譯大篇幅的錯誤信息
預編譯頭文件不在 c++標
十、模板基本術語
10.1 “類模板”還是“模板類”
10.2 替換,實例化和特例化
替換:在用模板實參去查找匹配的模板時,會嘗試用實參去替換模板參數,見 8.4 節。
實例化:查找到最匹配的模板后,根據實參從模板創建出常規類或函數的過程。
特例化:對模板中的部分或全部參數進行特化,定義新模板的過程。
10.3 聲明和定義
聲明:將一個名稱引入 c++作用域內,并不需要知道名稱的相關細節。
定義:如果在聲明時提供了細節,聲明就變成了定義。
10.4 唯一定義法則
唯一定義法制(ODR)
a.常規(比如非模板)非 inline 函數和成員函數,以及非 inline 的全局變量和靜態數據成員,在整個程序中只能被定義一次.
b. Class 類型(包含 struct 和 union),模板(包含部分特例化,但不能是全特例化),以及 inline 函數和變量,在一個編譯單元中只能被定義一次,而且不同編譯單元間的定義 應該相同.
10.5 模板參數和模板實參
1.模板參數:模板定義中模板參數列表中的參數。
模板實參:實例化模板參數時傳入的參數。
十一、泛型庫
11.1 可調用對象
c++可調用對象類型
a.函數指針
b. 仿函數
c. 存在一個函數指針或者函數引用的轉換函數的 class 類型
#include?<iostream> #include?<vector>template<typename?Iter,?typename?Callable> void?foreach?(Iter?current,?Iter?end,?Callable?op) {while?(current?!=?end)?{?//as?long?as?not?reached?the?endop(*current);?//?call?passed?operator?for?current?element++current;?//?and?move?iterator?to?next?element} }//?a?function?to?call: void?func(int?i) {std::cout?<<?"func()?called?for:?"?<<?i?<<?std::endl; } //?a?function?object?type?(for?objects?that?can?be?used?as?functions): class?FuncObj?{public:void?operator()?(int?i)?const?{?//Note:?const?member?functionstd::cout?<<?"FuncObj::op()?called?for:?"?<<?i?<<?std::endl;} }; int?main() {std::vector<int>?primes?=?{2,?3,?5,?7,?11,?13,?17,?19};foreach?(primes.begin(),?primes.end(),?func);?//?function?as?callable?(decays?to?pointer)foreach(primes.begin(),?primes.end(),?&func);?//?function?pointer?as?callableforeach(primes.begin(),?primes.end(),?FuncObj());?//?function?object?as?callableforeach(primes.begin(),?primes.end(),?[]?(int?i)?{?//lambda?as?callablestd::cout?<<?"lambda?called?for:?"?<<?i?<<?std::endl;}); }11.2 其它實現泛型庫的工具
標準庫中提供了豐富多樣的 type traits 工具,使用時一定要注意類型萃取的精確定義。
std::addressof<>()會返回一個對象或者函數的準確地址,即使一個對象重載了取地址運算符&也是這樣。
函數模板 std::declval()可以被用作某一類型的對象的引用的占位符。
11.3 完美轉發臨時變量
使用 auto &&可以創建一個可以被轉發的臨時變量。
11.4 其它實現泛型庫的工具
模板參數 T 的類型可能被推斷為引用類型,此時可能會引起意料之外的錯誤。
2.如果需要禁止引用類型進行實例化,可以使用 std::is_reference 進行判斷。
11.5 推遲計算
可以通過模板來延遲表達式的計算,這樣模板可以用于不完整類型。
十二、深入模板基礎
12.1 參數化聲明
C++ 目前支持四種基本類型的模板:類模板、函數模板、變量模板和別名模板。這些模板均可定義在全局作用域或者類作用域中。
除了類模板,也可以定義 union 模板
3.在類模板內,也可以定義和模板參數無關的非模板成員。
4.類內的模板函數不能是虛函數,普通函數可以是虛函數。
template<int?I> class?CupBoard { public:class?Shelf;?//?ordinary?class?in?class?templatevoid?open();?//?ordinary?function?in?class?templateenum?Wood{};?//?ordinary?enumeration?type?in?class?templatestatic?double?totalWeight;?//?ordinary?static?data?member?in?class?templatevirtual?void?fun(){};?//?普通函數可以是虛函數virtual?~CupBoard(){};?//?析構函數也可以是虛函數template?<typename?U>virtual?void?foo(const?U?&u){};?//?error?模板函數不能是虛函數 };每一個模板名稱需要在所在作用域內獨一無二。
函數模板可以有 c++鏈接,但不能有 C 鏈接。
函數模板一般具有外部鏈接,除非是 static 或定義在未命名的命名空間中。
12.2 模板參數
模板參數分為三種:類型模板參數,非類型模板參數和模板模板參數。
2.非類型模板參數可以是以下形式:
a.整型或枚舉類型
b.指針類型
c.指向類成員的指針
d.左值引用
e.std::nullptr_t
f.含有 auto 或者 decltype(auto)的類型(since c++17)
模版模板參數的關鍵字只能是 class 不能是 struct 或 union
4. c++11 開始,可以通過...定義模板參數包,匹配任意類型和數目的參數。
5. 也可以定義指定類型的非模板參數包,匹配指定類型任意數目的參數。
6. 模板可以提供模板參數的默認值,一旦為一個參數提供默認值,其后的參數都必須已經定義默認值。
7. 若一個模板存在多處聲明或聲明和定義同時存在,那么可以在這些地方定義模板參數的默認值,不能為一個參數重復定義默認值。
template<template<typename?X>?class?C>?//?ok void?f(C<int>*?p); template<template<typename?X>?struct?C>?//?error void?f(C<int>*?p); template<template<typename?X>?union?C>?//?error void?f(C<int>*?p); template<template<typename?X>?typename?C>?//?ok?since?c++17 void?f(C<int>*?p);template<template<typename?T,?T*>?class?Buf>?//?OK class?Lexer { static?T*?storage;?//?模板模板參數的參數不能在聲明之外使用 };template<typename?...Types>?//?模板參數包可以匹配任意參數 class?Tuple{};Tuple<int>?t1;?//?匹配int Tuple<int,?char>?t2;?//?匹配int?chartemplate<typename?T,?unsigned...?Dimensions>?//?非類型模板參數包匹配任意數目的unsigned參數 class?MultiArray?{}; MultiArray<double,?3,?3>?matrix;?//?定義矩陣template<typename?T1?=?int,?typename?T2>?//?error?T2未定義默認值 class?Test;template<typename?T1,?typename?T2?=?double>?//?ok class?Quintuple; template<typename?T1?=?int,?typename?T2>?//?ok?T2默認值前面已經定義 class?Quintuple; template<typename?T1?=?int>?//?error?不能重復定義默認值 class?Quintuple;12.3 模板實參
函數模板一般可以通過模板實參來推斷模板參數,但也存在無法推斷的情形。
模板模板參數的實參必須完全匹配模板模板參數。
12.4 可變參數模板
12.5 友元
將類模板作為友元時,必須保證友元定義位置已經知道類模板的聲明。
也可以將類型模板參數定義為友元 。
可以將函數模板定義為友元,此時若模板參數可推導,在友元聲明時可以省略。
十三、模板中的名稱
13.1 名稱分類
1.名稱分為受限名稱和非受限名稱,受限名稱前面有顯式的出現 ->, ::, . 這三個限定符
13.2 名稱查找
c++名稱的普通查找規則為從名稱所在的 scope 從內向外依次查找。
ADL( Argument-Dependent Lookup)查找為依賴于參數的查找,是用于函數調用表達式中查找非限定函數名稱的規則。當在使用函數的上下文中找不到函數定義,我們可以在其參數的關聯類和關聯名字空間中查找該函數的定義。
ADL 生效條件:a.使用此規則的函數必須要有參數 b. 使用此規則的函數名稱必須為非受限名稱
4.ADL 查找范圍:
(1)對于基本類型(int, char 等), 該集合為空集
(2)對于指針和數組類型,該集合是所引用類型的關聯類和關聯名字空間
(3)對于枚舉類型,名字空間是名字空間是枚舉聲明所在的名字空間,對于類成員,關聯類是枚舉所在的類
(4)對于 class(包含聯合類型),關聯類包括該類本身,他的外圍類,直接基類,間接基類。關聯名字空間包括每個關聯類所在的名字空間。
(5)對于函數類型, 該集合包含所有參數類型和返回類型的關聯類和關聯名字空間
(6)對于類 X 的成員指針類型,除了包括成員相關的關聯名字空間,關聯類,該集合還包括與 X 相關的關聯名字空間和關聯類
#include?<iostream> namespace?X?{template<typename?T>?void?f(T); } namespace?N?{using?namespace?X;enum?E?{?e1?};void?f(E)?{std::cout?<<?"N::f(N::E)?called\n";} } void?f(int) {std::cout?<<?"::f(int)?called\n"; } int?main() {::f(N::e1);?//?qualified?function?name:?no?ADLf(N::e1);?//?ordinary?lookup?finds?::f()?and?ADL?finds?N::f(),the?latter?is?preferred }c++ 類中聲明的友元函數在類外是不可見的,若未在類外提供定義,要想查找到該函數,只能通過 ADL.
類名注入:在類作用域中,當前類的名字被當做它如同是一個公開成員名一樣;這被稱為注入類名(injected-class-name)。
對于類模板,除了注入類名,還可以注入實例化的類名。
13.3 模板解析
大多數編程語言的編譯器的兩個基本活動是標記化和解析。
X<1>(0) ;被解析時,若 X 是一個類型,<1>則被認為是模板實參;若 X 不是類型,則被認為是 x<1 比較后的結果再和>0 比較。在處理模板時,一定要避免大于符號>被當做模板參數終止符。
類型名稱具有以下性質時,就需要在其前面添加 typename
a. 名稱出現在一個模板中
b. 名稱是受限的
c. 名稱不是用于基類的派生列表或構造函數的初始化列表中
d. 名稱依賴于模板參數
ADL 用于模板函數時,可能會產生錯誤。
13.4 派生和類模板
大多數情況中類模板派生和普通類派生無太大區別。
2.非依賴型基類:無需知道模板名稱就可以完全確定類型的基類。
3.非依賴型基類的派生類中查找一個非受限名稱時,會先從非依賴型基類中查找,然后才是模板參數列表。
template<typename?X> class?Base?{ public:int?basefield;using?T?=?int; };class?D1:?public?Base<Base<void>>?{?//?實際上不是模板 public:void?f()?{?basefield?=?3;?}?//?繼承成員普通訪問 }; template<typename?T> class?D2?:?public?Base<double>?{?//?繼承自非依賴型基類 public:void?f()?{?basefield?=?7;?}?//?繼承成員普通訪問T?strange;?//?T?是?Base<double>::T?類型,也就是int,?而不是模板參數T! };十四、實例化
14.1 On-Demand 實例化
1.模板被實例化時,編譯器需要知道實例化部分的完整定義。
14.2 延遲實例化
1.模板實例化存在延遲現象,編譯器只會實例化需要的部分。如類模板會只實例化用到的部分成員函數,函數模板如果提供了默認參數,也只會在這個參數會用到的時候實例化它。
14.3 c++實例化模型
1.兩階段查找:編譯器在模板解析階段會檢測不依賴于模板參數的非依懶型名稱,在模板實例化階段再檢查依懶型名稱。
2.Points of Instantiation: 編譯器會在需要實例化模板的地方插入實例化點(POI)
14.4 幾種實現方案
14.5 顯式實例化
十五、模板實參推導
15.1 推導的過程
1.函數模板實例化過程中,編譯器會根據實參的類型和模板參數 T 定義的形式,推導出函數的各個參數的類型,如果最后推導的結論矛盾,則推導失敗。
template<typename?T> T?max?(T?a,?T?b) {return?b?<?a???a?:?b; } auto?g?=?max(1,?1.0);?//?error?根據1推導T為int?根據1.0推導T為doubletemplate<typename?T>?void?f(T); template<typename?T>?void?g(T&); double?arr[20]; int?const?seven?=?7; f(arr);?//?T?被decay為double* g(arr);?//T?推斷為?double[20] f(seven);?//?T?被decay為int g(seven);?//?T?推斷為?int?const15.2 推導的上下文
15.3 特殊的推導情況
1.函數模板被取地址賦予函數指針時,也會產生實參推導。
2.類中定義了類型轉換的模板函數時,在類型轉換時可以產生實參推導。
template<typename?T> void?f(T,?T); void?(*pf)(char,?char)?=?&f;??//?T?被推斷為charclass?S?{ public:template<typename?T>?operator?T&(); };void?f(int?(&)[20]); void?g(S?s) {f(s);?//?類型轉換時T被推導為int[20] }15.4 初始化列表
1.模板實參如果是初始化列表時,無法直接完成模板參數類型 T 的推導。若函數參數通過 std::initializer_list 定義,則實參類型需要一致。
template<typename?T>?void?g(T?p); template<typename?T>?void?f(std::initializer_list<T>);int?main()?{g({’a’,?’e’,?’i’,?’o’,?’u’,?42});?//?ERROR:?T?deduced?to?both?char?and?intf({1,?2,?3});?//?ERROR:?cannot?deduce?T?from?a?braced?listf({2,?3,?5,?7,?9});?//?OK:?T?is?deduced?to?int }15.5 參數包
15.6 右值引用
1.引用折疊:只有兩個右值引用會被折疊為右值引用,其它情形都是左值引用
undefinedusing?RCI?=?int?const&; RCI?volatile&&?r?=?42;?//?OK:?r?has?type?int?const& using?RRI?=?int&&; RRI?const&&?rr?=?42;?//?OK:?rr?has?type?int&&15.7 SFINAE
1.根據 SFINAE 原理,編譯器在用實參推導模板參數失敗時,會將該模板忽略。
template<typename?T,?unsigned?N> T*?begin(T?(&array)[N]) {return?array; } template<typename?Container> typename?Container::iterator?begin(Container&?c) {return?c.begin(); } int?main() {std::vector<int>?v;int?a[10];::begin(v);?//?OK:?only?container?begin()?matches,?because?the?first?deduction?fails::begin(a);?//?OK:?only?array?begin()?matches,?because?the?second?substitution?fails }十六、特化和重載
16.1 當泛型代碼不再適用的時候
16.2 重載函數模板
1.函數模板和普通函數一樣,是可以被重載的。
#include<iostream>template<typename?T> int?f(T) {return?1; } template<typename?T> int?f(T*) {return?2; }int?main() {std::cout?<<?f<int*>((int*)0)?<<?std::endl;?//?更匹配第一個函數calls?f<T>(T)std::cout?<<?f<int>((int*)0)?<<?std::endl;?//?只匹配第二個函數?calls?f<T>(T*)std::cout?<<?f((int*)0)?<<?std::endl;?//?都匹配,但第二個更特殊,優先選用?calls?f<T>(T*) }如上所示,main 中實例化后的前兩個函數完全相同,但是可以同時存在,原因是它們具有不同的簽名。
3.函數簽名由以下部分構成:
a. 非受限函數名稱
b. 名稱所屬的類作用域
c. 函數的 const volatile 限定符
d. 函數參數的類型
e. 如果是函數模板,還包括返回類型、模板參數和模板實參。
//?以下模板的實例化函數可以同時存在 template<typename?T1,?typename?T2> void?f1(T1,?T2); template<typename?T1,?typename?T2> void?f1(T2,?T1); template<typename?T> long?f2(T); template<typename?T> char?f2(T);c++最開始例子中最后一個函數實例化時和兩個模板均匹配,此時將選用更特殊的函數模板。
普通函數和模板函數也可以同時重載,此時在匹配程度相同時,優先調用普通函數。
16.3 顯式特化
重載只適用于函數模板,對于類模板,可以使用特化的方式使得編譯器進行更優的選擇。
類模板若提供了默認參數,特化后的模板可以不加。
模板全特化之后的類和由相同的特化參數實例化后的類是相同的,不能同時存在。
類模板全特化后,若在類外定義函數,則不能在前面加 template<>
函數模板全特化時,不能含有默認參數值,但可以直接使用。
變量模板也可以進行全特化。
除了對類模板進行全特化以外,也可以單獨為類模板中的某個成員進行全特化。
16.4 類模板偏特化
對于類模板,可以進行偏特化,偏特化也不能提供默認參數,但可以直接使用。
16.5 變量模板偏特化
對于變量模板,也可以偏特化。
十七、未來的方向
十八、模板的多態性
18.1 動態多態
動態多態:通過繼承和虛函數實現,在運行期根據指針或引用的具體類型決定具體調用那一個虛函數。
18.2 靜態多態
靜態多態:通過模板實現,在編譯期基于類型調用不同模板。
18.3 動態多態 vs 靜態多態
18.4 使用 concepts
使用靜態多態時可以采用 6.5 中介紹的 concept 對可傳入模板的類型做以限制。
18.5 新形式的設計模式
對于橋接模式,若具體實現的類型在編譯期間可以確定,則可以使用模板代替傳統的橋接模式實現。
18.6 泛型編程
十九、萃取實現
19.1 序列求和示例
簡單的求和模板函數的定義及使用如下:
為了避免求和溢出,可以通過下面簡單萃取的方式重新定義求和函數。
上面例子還可能出現問題,就是初始化 total 時,有些類型可能會并不支持默認初始化。為了兼容這種情形,可采用值萃取的形式提供默認初始值。
上面類內初始化靜態數據成員的方式只對整型有效,對于 float 和字面值常量,可以通過 constexpr 定義進行類類初始化,對于非字面值的類型,則可以通過 inline 成員函數提供類內定義。
也可以將上述的萃取形式參數化,便于特殊情形下指定不同的萃取形式。
19.2 萃取 vs 策略或策略類
對于 19.1 中的 accum 函數,可以指定不同策略,不止用來求和,可以求乘。
萃取偏向于模板參數本質的特性,策略偏向于泛型可配置的行為。
19.3 類型函數
類型函數:接收一些類型作為參數,返回類型或常量值為結果。
萃取還可以用來為類型添加或移除引用,const,volatile 等限定符。
除了對單個類型進行萃取,也可以通過萃取對多個類型進行預測。
對 3 中的 same 判斷可以進一步優化,以實現編譯期多態。
19.4 基于 SFINAE 的萃取
可以基于 SFINAE 原理排除某些重載的函數模板。
可以基于 SFINAE 原理排除某些重載的類模板。
19.5 IsConvertibleT
19.6 探測成員
1.可以基于 SFINAE 原理探測類型 T 中是否含有名為 X 的成員。
//?helper?to?ignore?any?number?of?template?parameters:template<typename...>?using?VoidT?=?void; //?primary?template:template<typename,?typename?=?VoidT<>>struct?HasSizeTypeT?:?std::false_type{}; //?只有含有size_type時才會替換成功,其余情況偏特化的模板被忽略template<typename?T>struct?HasSizeTypeT<T,?VoidT<typename?T::size_type>>?:?std::true_type { };std::cout?<<?HasSizeTypeT<int>::value;?//?false struct?CX?{using?size_type?=?std::size_t; }; std::cout?<<?HasSizeType<CX>::value;?//?true2.對于 13.2 中的介紹的注入的類名詞,上述檢查結果為 true。
struct?size_type?{?}; struct?Sizeable?:?size_type?{?}; std::cout?<<?HasSizeTypeT<Sizeable>::value;?//ture可以將萃取的類型名稱參數化,通過宏定義適用于任何類型參數的萃取
除了探測類型成員,也可以探測非類型成員
19.7 其他的萃取技術
1.if-then-else:接收一個條件參數,根據條件從兩個類型參數中做選擇。
template<bool?COND,?typename?TrueType,?typename?FalseType> struct?IfThenElseT?{using?Type?=?TrueType; }; //?模板偏特化 template<typename?TrueType,?typename?FalseType>?struct?IfThenElseT<false,?TrueType,?FalseType>?{using?Type?=?FalseType; }; template<bool?COND,?typename?TrueType,?typename?FalseType> using?IfThenElse?=?typename?IfThenElseT<COND,?TrueType,?FalseType>::Type;19.8 類型分類
19.9 策略萃取
19.10 在標準庫中的情形
二十、基于類型屬性的重載
20.1 算法特化
可以通過給函數模版提供不同形式的參數實現算法特化。
20.2 標記派發
stl 中根據迭代器類型調用不同接口實現 itr+n
20.3 enable\disable 函數模板
二十一、模板和繼承
21.1 空基類優化
c++中對于空類,為保證類對象有唯一地址,會為其插入一子節的內存。
空基類優化:在空類作為基類時,如果為它不分配內存不會導致它存儲到其他同類型對象或者子類型對象的相同地址上,則可以不分配。
21.2 CRTP
crtp:將派生類類型作為模板參數傳遞給基類。
21.3 混入
21.4 命名的模板參數
二十二、橋接靜態多條和動態多態
22.1 函數對象,指針及 std::function<>
與 C++函數指針相比,std::functional<>還可以被用來存儲 lambda,以及其它任意 實現了合適的 operator()的函數對象。
二十三、模板元編程
22.1 c++元編程現狀
1.值元編程:在編譯期通過模板基于輸入參數計算得到結果值。
//?計算平方根的模板 template<typename?T> constexpr?T?sqrt(T?x) {if?(x?<=?1)?{return?x;}T?lo?=?0,?hi?=?x;for?(;;){auto?mid?=?(hi+lo)/2,?midSquared?=?mid*mid;if?(lo+1?>=?hi?||?midSquared?==?x)?{return?mid;}if?(midSquared?<?x)?{?lo?=?mid;}?else?{hi?=?mid;}} }sqrt(9);?//?編譯期計算結果2.類型元編程:在編譯期通過模板基于輸入類型得到輸出類型。
//?primary?template:?in?general?we?yield?the?given?type: template<typename?T>?struct?RemoveAllExtentsT?{using?Type?=?T; }; //?partial?specializations?for?array?types?(with?and?without?bounds): template<typename?T,?std::size_t?SZ> struct?RemoveAllExtentsT<T[SZ]>?{using?Type?=?typename?RemoveAllExtentsT<T>::Type; }; template<typename?T> struct?RemoveAllExtentsT<T[]>?{using?Type?=?typename?RemoveAllExtentsT<T>::Type; }; template<typename?T> using?RemoveAllExtents?=?typename?RemoveAllExtentsT<T>::Type;混合元編程:混合值和類型元編程。
參考資料:
1.c++ templates 第二版
2.c++ templates 第二版翻譯.知乎
最新一期有料程序員直播預告
總結
- 上一篇: Web内核微信小程序框架实践
- 下一篇: 从C++转向最受欢迎的Rust语言