Loading... # 一:基本范例 1. 模板的定义是以template关键字开头。 2. **类型模板参数T前面用typename来修饰**,所以,遇到typename就该知道其后面跟的是一个类型。**其中,typename可以被class取代,但此处的class并没有“类”的概念,只是为了后面修饰T**。 3. 类型模板参数T(代表是一个类型)以前前面的修饰符typename/class都用<>括起来 4. T这个名字可以换成任意其他标识符,对程序没有影响。用T只是一种编程习惯。 # **二:实例化** 1. 实例化:编译时,用具体的“类型”代替“类型模板参数”的过程就叫做实例化(有人也叫代码生成器)。 2. 可以通过objdump来查看是否有被实例化 (windows下使用使用dumpbin把二进制文件导入到文本中查看) ```cpp int Sub(int tv1, int tv2) { return tv1 - tv2; } float Sub(float tv1, float tv2) { return tv1 - tv2; } ``` 使用函数模板后 ```cpp template <class T> //class 可以取代typename,但是这里的class并没有“类”这种含义 T Sub(T tv1, T tv2) { return tv1 - tv2; } ``` T这个名字可以任意起 叫U...都可以 ```cpp #include <iostream> using namespace std; template <class T> //class 可以取代typename,但是这里的class并没有“类”这种含义 T Sub(T tv1, T tv2) { return tv1 - tv2; } int main() { Sub(2,1); Sub(2.2,1.1); return 0; } ``` 使用objdump -S 2\_1 > 2\_1.txt 之后 ```cpp 000000000000120a <_Z3SubIiET_S0_S0_>: 120a: f3 0f 1e fa endbr64 120e: 55 push %rbp 120f: 48 89 e5 mov %rsp,%rbp 1212: 89 7d fc mov %edi,-0x4(%rbp) 1215: 89 75 f8 mov %esi,-0x8(%rbp) 1218: 8b 45 fc mov -0x4(%rbp),%eax 121b: 2b 45 f8 sub -0x8(%rbp),%eax 121e: 5d pop %rbp 121f: c3 retq 0000000000001220 <_Z3SubIdET_S0_S0_>: 1220: f3 0f 1e fa endbr64 1224: 55 push %rbp 1225: 48 89 e5 mov %rsp,%rbp 1228: f2 0f 11 45 f8 movsd %xmm0,-0x8(%rbp) 122d: f2 0f 11 4d f0 movsd %xmm1,-0x10(%rbp) 1232: f2 0f 10 45 f8 movsd -0x8(%rbp),%xmm0 1237: f2 0f 5c 45 f0 subsd -0x10(%rbp),%xmm0 123c: 5d pop %rbp 123d: c3 retq 123e: 66 90 xchg %ax,%ax ``` 使用objdump -s 2\_1.o > 2\_1\_1.txt之后 {这里会实例化两个} ```cpp peter@iZbp1aq6c9kbbder4q405mZ:~/template_class/template_demo/2part$ nm 2_1.o U __cxa_atexit U __dso_handle U _GLOBAL_OFFSET_TABLE_ 0000000000000088 t _GLOBAL__sub_I_main 0000000000000000 T main 0000000000000000 W _Z3SubIdET_S0_S0_ 0000000000000000 W _Z3SubIiET_S0_S0_ 000000000000003b t _Z41__static_initialization_and_destruction_0ii U _ZNSt8ios_base4InitC1Ev U _ZNSt8ios_base4InitD1Ev 0000000000000000 r _ZStL19piecewise_construct 0000000000000000 b _ZStL8__ioinit ``` **W或w :符号为弱符号,当系统有定义符号时,使用定义符号,当系统未定义符号且定义了弱符号时,使用弱符号**。 如果不实例化模板的话就不会有这 两个函数,注意实例化后的名字 1. 通过函数模板实例化之后的函数名包含三部分: 3.1 模板名{sub};b)后面跟着一对<>{可能带有多个参数};c)<>中间是一个具体类型。 3.2 编译期间。 在编译阶段,编译器就会查看函数模板的 函数体部分,来确定能否针对该类型string进行Sub函数模板的实例化。 在编译阶段,编译器需要能够找到函数模板的函数体部分。{比如-【减号】的时候针对string就不行,并且不会自动进行隐式转换,除非自己强制转化} -->如果找不到的话就会编译报错 ```cpp string a("abc"), b("def"); string addresult = Sub(a, b);// --> 这种情况在编译期间就会报错 error: no match for ‘operator-’ (operand types are ‘std::__cxx11::basic_string<char>’ and ‘std::__cxx11::basic_string<char>’) ``` 编译报错的原因:编译时就会出现这种报错 ### **关键问题:`std::string` 没有定义减法运算符()** 模板函数 `Sub` 的核心操作是 `tv1 - tv2`,这依赖于类型 `T` 必须支持**二元减法运算符**(`operator-`)。对于基本类型(如 `int`、`double` 等),编译器内置了减法运算符的支持;但对于 `std::string` 类型,**标准库并未为其定义减法运算符**。 # 三:模板参数的推断 1. 常规的参数推断 通过<>可以只指定一部分模板参数的类型,另外一部分模板参数的类型可以通过调用时给的实参来推断。 auto代替函数模板返回类型 **decltype,可以与auto结合使用来构成返回类型后置语法。这种后置语法其实也就是使用auto和decltype结合来完成返回值类型的推导**。 ```cpp template <typename T, typename U> //auto Add(T tv1, U tv2) //auto用于表达推导返回值类型的含义。 auto Add(T tv1, U tv2) -> decltype(tv1 + tv2) //返回类型后置语法,这里的auto只是返回类型后置语法的组成部分,并没有类型推导的含义 { return tv1 + tv2; } int main() { Add(1, 2); return 0; } ``` 2. 各种推断的比较以及空模板参数列表的推断 2.1 自动推断 ```cpp int subv = Sub(3, 5); int subv = Sub(double(3), 5.9);//使用double()强制类型转换可以 要求类型自动匹配,无法进行类型转换,所以这里必须加上double进行强转 double subv = Sub<double>(3, 5.9);//直接指定<double> ``` 2.2 指定类型模板参数,优先级比自动推断高 Sub<int>(2.2, 1.1); 编译器不会根据2.2 1.1形参 double去推导,而是使用指定的int优先级更高 ```cpp template <class T> //class 可以取代typename,但是这里的class并没有“类”这种含义 T Sub(T tv1, T tv2) { cout << __func__<< " : "<<tv1 - tv2 <<endl; return tv1 - tv2; } Sub<int>(2.2, 1.1);//Sub : 1 会使用<>里面的int去推导 ``` 2.3 指定空模板参数列表<>:作用就是 **请调用mydouble函数模板而不是调用普通的mydouble函数**。 ```cpp template <typename T> T mydouble(T tmpvalue) { cout << __LINE__<< endl; return tmpvalue * 2; } double mydouble(double tmpvalue)//这个是普通函数,不是特化 { cout << __LINE__<< endl; return tmpvalue * 2; } int main() { auto result4 = mydouble(16.9); //调用的是普通函数,对于编译器,模板也合适,普通也合适,编译器会优先调用普通函数 auto result3 = mydouble<>(16.9); //空的<>没有用处, <>作用就是告诉编译器,调用mydouble函数模板。 return 0; } ``` 1. 当存在默认类型的时候 ```cpp template < typename V,typename T, typename U> V Add1(T tv1, U tv2) { cout << __LINE__<< endl; return tv1 + tv2; } int main() { //cout << Add1(15, 17.8) << endl;//{会编译报错 因为V无法推导出来} //cout << Add1<...,...,double>(15, 17.8) << endl;//{编译器没有这种语法 只指定V,不指定T 和U} 当为template < typename T, typename U, typename V>的时候 cout << Add1<int,double,double>(15, 17.8) << endl;//{solution1:给出所有类型} cout << Add1<double>(15, 17.8) << endl;//{solution2变化一下变量的位置 把V 移动到最前面template < typename V,typename T, typename U>} ,那么只指定第一个模板参数,那么后面的两个变量就可以使用自动推导 return 0; } ``` 还有一种最好的solution,去除V这个参数 直接使用auto+decltype进行自动推导 ```jsx template < typename V,typename T, typename U> auto Add1(T tv1, U tv2)//auto代替函数模板返回值 { cout << __LINE__<< endl; return tv1 + tv2; } int main() { cout << Add1(15, 17.8) << endl; return 0; } ``` # 四、重载 > 函数(函数模板)名字相同,但是**参数数量或者参数类型上**不同。 函数模板和函数也可以同时存在,此时可以把函数看成是一种重载,当普通函数和函数模板都比较合适的时候,**编译器会优先选择普通函数来执行**。{避免这种方式就是使用<>} ```cpp template<typename T> void myfunc(T tmpvalue) { cout << "myfunc(T tmpvalue)执行了" << endl; } //函数模板的重载 template<typename T> void myfunc(T* tmpvalue) { cout << "myfunc(T* tmpvalue)执行了" << endl; } //也能看做是函数的重载 void myfunc(int tmpvalue) { cout << "myfunc(int tmpvalue)执行了" << endl; } int main() { myfunc(12);//myfunc(int tmpvalue)执行了 char* p = nullptr; myfunc(p);//myfunc(T* tmpvalue)执行了 myfunc(12.1);//myfunc(T tmpvalue)执行了 } ``` 如果选择最合适(最特殊)的函数模板/函数,编译器内部有比较复杂的排序规则,规则也在不断更新。 # 五、特化 泛化(泛化版本):大众化的,常规的。常规情况下,写的函数模板都是泛化的函数模板。 特化(特化版本):往往代表着从泛化版本中抽出来的一组子集。 ## 全特化 全特化实际上等价于实例化一个函数模板,并不等价于一个函数重载。 void tfunc<int ,double>(int& tmprv, double& tmprv2){......} //全特化的样子 void tfunc(int& tmprv, double& tmprv2) { ...... } //重载函数的样子{并没有<>} ```cpp //泛化版本{如何区分是否是泛化还是特化的 就是函数名后面是否有<> 存在的话 就是特化版本} template <typename T,typename U> //T = const char *;U = int void tfunc(T& tmprv, U& tmprv2) //tmprv = const char * &{指针的别名},tmprv2 = int &{左值引用} { cout << "tfunc泛化版本" << endl; cout << tmprv << endl; cout << tmprv2 << endl; } template<typename T> void myfunc(T* tmpvalue) { cout << "myfunc(T* tmpvalue)执行了" << endl; } //全特化版本{登记于实例化一个函数模板} template <> //全特化<>里面为空 void tfunc<int ,double>(int& tmprv, double& tmprv2)//<int, double>可以省略,因为根据实参可以完全推导出T和U的类型。 //void tfunc(int& tmprv, double& tmprv2) { cout << "---------------begin------------" << endl; cout << "tfunc<int,double>特化版本" << endl; cout << tmprv << endl; cout << tmprv2 << endl; cout << "---------------end------------" << endl; } int main() { //泛化版本 const char* p = "I Love China!"; int i = 12; tfunc(p, i); //特化版本 但是如果存在普通函数的话,会先调用普通重载函数 int k = 12; double db = 15.8; tfunc(k, db); //泛化版本 const int k2 = 12; tfunc(k2, db); return 0; } ``` 编译器考虑的顺序:**优先选择普通函数,然后才会考虑函数模板的特化版本{但是当函数调用的时候**,遇到两个特化版本都比较合适的时候,比如函数调用传递字符串,有一个特化版本形参有指针 一个特化版本是数组,数组的特化版本比指针的特化版本更合适,编译器就会选择数组的},最后才会考虑函数模板的泛化版本。 **在上面的版本中额外增加这个函数:** ```cpp //重载函数 void tfunc(int& tmprv, double& tmprv2) { cout << "---------------begin------------" << endl; cout << "tfunc普通函数" << endl; cout << "---------------end------------" << endl; } int main() { //特化版本 但是如果存在普通函数的话,会先调用普通重载函数 int k = 12; double db = 15.8; tfunc(k, db); return 0; } ``` ## 偏特化 > **函数模板没有数量上的偏特化**,但是有范围上的偏特化。 **类模板有数量上的偏特化**,也有范围上的偏特化 从两方面来说:一个是模板参数数量上的偏特化,一个是模板参数范围上的偏特化。 1. 模板参数数量上的偏特化{要有泛化才会有偏特化}:比如针对tfunc函数模板,第一个模板参数类型为double,第二个模板参数不特化; 实际上,**从模板参数数量上来讲,函数模板不能偏特化。否则会导致编译错误**。 ```cpp //泛化版本{如何区分是否是泛化还是特化的 就是函数名后面是否有<> 存在的话 就是特化版本} template <typename T,typename U> //T = const char *;U = int void tfunc(T& tmprv, U& tmprv2) //tmprv = const char * &{指针的别名},tmprv2 = int &{左值引用} { cout << "tfunc泛化版本" << endl; cout << tmprv << endl; cout << tmprv2 << endl; } //从模板参数数量上的偏特化 根本编译就会报错, 下面这种写法会报错 template <typename U> void tfunc<double, U>(double& tmprv, U& tmprv2) { //....... } //但是可以把它写成缺省的 template <typename T = double, typename U> void tfunc(double& tmprv, U& tmprv2) { } ``` 2. 模板参数范围上的偏特化: 范围上:int->const int,类型变小; T->T\*,T->T&,T->T&&。针对T类型,从类型范围上都变小了。 ```cpp //泛化版本 template<typename T> void myfunc(T tmpvalue) { cout << "myfunc(T tmpvalue)执行了" << endl; } template<typename T> void myfunc(T* tmpvalue) { cout << "myfunc(T* tmpvalue)执行了" << endl; } //全特化版本{登记于实例化一个函数模板} template <> //全特化<>里面为空 void tfunc<int ,double>(int& tmprv, double& tmprv2)//<int, double>可以省略,因为根据实参可以完全推导出T和U的类型。 //void tfunc(int& tmprv, double& tmprv2) { cout << "---------------begin------------" << endl; cout << "tfunc<int,double>特化版本" << endl; cout << tmprv << endl; cout << tmprv2 << endl; cout << "---------------end------------" << endl; } //范围上的偏特化,实际上就是重载 template <typename T, typename U> void tfunc(const T& tmprv, U& tmprv2) { cout << "偏特化,本质上tfunc(const T& tmprv, U& tmprv2)重载版本" << endl; } int main() { //泛化版本 const char* p = "I Love China!"; int i = 12; tfunc(p, i); //偏特化版本 const int k2 = 12; tfunc(k2, db);//偏特化,本质上tfunc(const T& tmprv, U& tmprv2)重载版本 return 0; } ``` 实际上,**对于函数模板来讲,也不存在模板参数范围上的偏特化。因为这种所谓模板参数范围上的偏特化,实际上是函数模板的重载**。 1. 通过重载实现模板参数数量上的偏特化 使用函数重载实现偏特化 ```cpp //全特化版本{登记于实例化一个函数模板} template <> //全特化<>里面为空 void tfunc<int ,double>(int& tmprv, double& tmprv2)//<int, double>可以省略,因为根据实参可以完全推导出T和U的类型。 //void tfunc(int& tmprv, double& tmprv2) { cout << "---------------begin------------" << endl; cout << "tfunc<int,double>特化版本" << endl; cout << tmprv << endl; cout << tmprv2 << endl; cout << "---------------end------------" << endl; } template <typename U> void tfunc(double& tmprv, U& tmprv2) { cout << "---------------begin------------" << endl; cout << "类似于tfunc<double, U>偏特化的tfunc重载版本" << endl;{偏特化的tfunc重载版本} cout << tmprv << endl; cout << tmprv2 << endl; cout << "---------------end------------" << endl; } int main() { double j = 18.5; int i = 12; tfunc(j, i); return 0; } ``` # 六、缺省参数 {前后缺省都行} ```cpp typedef int(*FunType)(int, int); //函数指针类型定义 template <typename T,typename F = FunType>//{这里代表的是一个类型} void testfunc(T i, T j, F funcpoint = mf)//{mf是函数名 代表函数首地址} { cout << funcpoint(i, j) << endl; } template <typename T = int, typename U> void testfunc2(U m) { T tmpvalue = m; cout << tmpvalue << endl; } int main() { testfunc(15, 16, mf2); testfunc2(12); } ``` 使用默认参数可以,但是不能使用缺省参数 ```cpp //从模板参数数量上的偏特化 根本编译就会报错 template <typename U> void tfunc<double, U>(double& tmprv, U& tmprv2) { //....... } 编译报错: peter@iZbp1aq6c9kbbder4q405mZ:~/template_class/template_demo/2part$ g++ 2_1.cpp -o 2_1 2_1.cpp:99:47: error: non-class, non-variable partial specialization ‘tfunc<double, U>’ is not allowed 99 | void tfunc<double, U>(double& tmprv, U& tmprv2) | ^ //但是可以把它写成缺省的 编译可以通过 template <typename T = double, typename U> void tfunc(double& tmprv, U& tmprv2) { } ``` # 七:非类型模板参数 (7.1)基本概念 前面的函数模板涉及到的 模板参数都是 “类型 模板参数”需要用typename(class)来修饰。 模板参数还可以是 “非类型 模板参数(普通的参数)” {demo:namespace \_nmsp2} c++17开始,支持非类型模板参数为auto类型。{template < typename T, typename U, auto val = 100>} ```jsx template <typename T, typename U, int val = 100> auto Add(T tv1, U tv2) { return tv1 + tv2; } ``` 指定非类型模板参数的值时,一般给的都是常量{给变量是不行的}。因为编译器在编译的时候就要能够确定非类型模板参数的值。 并不是任何类型的参数都可以作为非类型模板参数。int类型可以,但double,float或者类类型string等等可能就不可以,不过double \*这种指针类型可以。 非类型模板参数有一些限制: 1. 必须是编译时常量 2. 不能是浮点类型(如float、double) 3. 不能是类或结构体类型(除非它们有静态成员转换为允许的类型) 4. 某些类型需要进行地址比较 最后修改:2025 年 06 月 30 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏