C++函数模板
第一节-函数模板
- 模板的定义是以
template
关键字开头; - 类型模板参数T前面用
typename
来修饰,所以,遇到typename
就该知道其后面跟的是一个类型。typename
可以被class取代,但此处的class并没有“类”; - 类型模板参数
T(代表是一个类型)
以前前面的修饰符typename/class都用<>括起来
- T这个名字可以换成任意其他标识符,对程序没有影响。用T只是一种编程习惯。
例子:
namespace _nmsp1
{
int Sub(int tv1, int tv2)
{
return tv1 - tv2;
}
float Sub(float tv1, float tv2)
{
return tv1 - tv2;
}
}
使用函数模板:
namespace _nmsp1
{
template <typename T> //T:称为 类型 模板参数,代表的是一个类型。
// template <class T> //class 可以取代typename,但是这里的class并没有“类”的含义,只是表示后面的T是一 //个类型,T这个名字可以任意起
T Sub(T tv1, T tv2) //给进去的T必须支持减法操作,编译器会做检查
{
return tv1 - tv2;
}
}
实例化
实例化:在编译器编译的时候,用具体的类型来代替”类型模板参数“的过程(有人也叫代码生成器)
VS上面通过dumpbin命令查看.obj文件可以查看到
.obj文件的格式一般会被认为是一种COFF----通用对象文件格式(Common Object File Format)。
Linux可以看.o文件
int Sub<int>(int,int)
double Sub<double>(double,double)
实例化之后的函数名分别叫做Sub<int>和Sub<double>
通过函数模板实例化之后的函数名包含三部分:
- 模板名;
- 后面跟着一对<>;
- <>中间是一个具体类型。
编译期间->实例化具体的sub函数
在编译阶段,编译器就会查看函数模板的函数体部分,来确定能否针对该类型string进行Sub函数模板的实例化。
在编译阶段,编译器需要能够找到函数模板的函数体部分。
在工程中,需要将函数模板包括头和体都要包含在头文件中;
模板参数的推断
-
常见的参数推断
namespace _nmsp1 { //------------------------------------------------- template <typename T,typename U,typename V> V Add(T tv1, U tv2) { return tv1 + tv2; } template <typename V,typename T,typename U> V Add1(T tv1, U tv2) { return tv1 + tv2; } template <typename T, typename U> auto Add2(T tv1, U tv2) // auto用于表达式推导返回类型的含义 { return tv1 + tv2; } template <typename T, typename U> auto Add3(T tv1, U tv2)-> decltype(tv1 + tv2) // 返回类型后置语法,这里的auto只是返回类型 { // 后置语法的一部分,并没有类型推导的含义 return tv1 + tv2; } } int main() { cout << _nmsp1::Add(15, 17,8) << endl; // error: _nmsp1::Add未能找到匹配的重载函数 // 未能为"V"推导模板参数,没有能提推断出返回值 // 怎么改呢? // cout << _nmsp1::Add<...,...double>(15, 17.8)没有这种语法 cout << _nmsp1::Add<int, double, double>(15, 17.8); // ok麻烦可可以值指定一部分模板参数嘛? // 方法二: cout << _nmsp1::Add1<double>(15, 17.8); // // 通过<>可以只指定一部分模板参数的类型,另外一部分模板参数的类型可以通过调用时给的实参来推断。但是一旦开始了推断就必须让它都是自动推断 // 方法三: cout << _nmsp1::Add2(15, 17.8) << endl; // 使用auto是可以的,C++17里面增强auto的功能 // decltype,可以与auto结合使用来构成返回类型后置语法。 // 这种后置语法其实也就是使用auto和decltype结合来完成返回值类型的推导。 cout << _nmsp1::Add3(15, 17.8) << endl; return 0; }
-
各种推断的比较以及空模板参数列表的推断
- 自动推断
- 指定类型模板参数,优先级比自动推断高
- 指定空模板参数列表<>:作用就是请调用mydouble函数模板而不是调用普通的mydouble函数。
namespace _nmsp1
{
template <typename T>
T mydouble(T tmpvalue)
{
return tmpvalue * 2;
}
double mydouble(double tmpvalue)
{
return tmpvalue * 2;
}
}
int main()
{
cout << _nmsp1::mydouble(15) << endl; // 可以推断出T类型是int类型
int result2 = _nmsp1::mydouble<int>(16.9); // 显示指定了类型有警告double->int
auto result3 = _nmsp1::mydouble<>(16.9) // 这种写法编译器是通过16.9推导是double类型
// 此种场合下,空的<>没有用处,但语法上允许
auto result4 = _nmsp1::mydouble(16.9); // 在模板和普通函数都适合的情况下,编译器会优先调 // 用普通函数
auto result5 = _nmsp1::mydouble<>(16.9); // 告诉编译器调用模板函数
reutrn 0;
}
重载
-
函数(函数模板)名字相同,但是参数数量或者参数类型上不同。
-
函数模板和函数也可以同时存在,此时可以把函数看成是一种重载,当普通函数和函数模板都比较合适的时候,编译器会优先选择普通函数来执行。
-
如果选择最合适(最特殊)的函数模板/函数,编译器内部有比较复杂的排序规则,规则也在不断更新。
namespace _nmsp1
{
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()
{
_nmsp1::myfunc(12); // int,优先普通函数版本
char*p = nullptr;
_nmsp1::myfunc(p); // T char* char
_nmsp1::mufunc(12.1); //T double
}
特化
泛化(泛化版本):大众化的,常规的。常规情况下,写的函数模板都是泛化的函数模板。
特化(特化版本):往往代表着从泛化版本中抽出来的一组子集。
实例化:tfunc<const char*, int>
namespace _nmsp2
{
//泛化版本
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;
}
}
int main()
{
const char* p = "I love China!";
int i = 12;
_nmsp2::tfunc(p, i); // 输出泛化版本
return 0;
}
- 全特化:就是把tfunc这个泛化版本中的所有模板参数都用具体的类型来代替构成的一个特殊的版本(全特化版本);
全特化实际上等价于实例化一个函数模板,并不等价于一个函数重载。
void tfunc<int ,double>(int& tmprv, double& tmprv2){......} // 全特化的样子
void tfunc(int& tmprv, double& tmprv2) { ...... } // 重载函数的样子
编译器考虑的顺序:优先选择普通函数,然后才会考虑函数模板的特化版本,最后才会考虑函数模板的泛化版本。
namespace _nmsp2
{
//泛化版本
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 <> // 全特化<>里面为空
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;
}
//重载函数
void tfunc(int& tmprv, double& tmprv2)
{
cout << "---------------begin------------" << endl;
cout << "tfunc普通函数" << endl;
cout << "---------------end------------" << endl;
}
}
int main()
{
int k = 12;
double db = 15.8;
_nmsp2::tfunc(k, db); // 执行特化版本,普通函数版本优先
return 0;
}
-
偏特化(局部特化)
从两方面来说:一个是模板参数数量上的偏特化,一个是模板参数范围上的偏特化。
- 模板参数数量上的偏特化:比如针对
tfunc
函数模板,第一个模板参数类型为double
,第二个模板参数不特化;实际上,从模板参数数量上来讲,函数模板不能偏特化。否则会导致编译错误。 - 模板参数范围上的偏特化:范围上:
int->const int
,类型变小;T->T*,T->T&,T->T&&
。针对T类型,从类型范围上都变小了。实际上,对于函数模板来讲,也不存在模板参数范围上的偏特化。因为这种所谓模板参数范围上的偏特化,实际上是函数模板的重载。 - 通过重载实现模板参数数量上的偏特化;
- 模板参数数量上的偏特化:比如针对
namespace _nmsp2
{
//泛化版本
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> // error编译问题,参数数量不能偏特化
//void tfunc<double, U>(double& tmprv, U& tmprv2)
//{
// //.......
//}
template <typename U>
void tfunc(double& tmprv, U& tmprv2)
{
cout << "---------------begin------------" << endl;
cout << "类似于tfunc<double, U>偏特化的tfunc重载版本" << 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 int k2 = 12;
_nmsp2::tfunc(k2, db); // tfunc(const T& tmprv, U& tmprv2)重载版本
return 0;
}
- 结论:函数模板不能偏特化
后续讲解类模板时,对于类模板,还是存在模板参数范围上的偏特化以及数量上的偏特化,后续会详细讲解。
缺省参数
namespace _nmsp1
{
//普通函数
int mf(int tmp1, int tmp2)
{
return 1;
}
int mf2(int tmp1, int tmp2)
{
return 10;
}
typedef int(*FunType)(int, int); //函数指针类型定义
template <typename T,typename F = FunType> // FunType类型
void testfunc(T i, T j, F funcpoint = mf) // 缺省值mf
{
cout << funcpoint(i, j) << endl; // call mf函数
}
//------------------
template <typename T = int, typename U>
void testfunc2(U m)
{
T tmpvalue = m;
cout << tmpvalue << endl;
}
}
int main()
{
_nmsp1::testfunc(15, 16, _nmsp1::mf2);
_nmsp1::testfunc2(12); // 12
return 0;
}
非类型模板参数
-
基本概念
-
前面的函数模板涉及到 模板参数都是 “类型 模板参数”需要用typename(class)来修饰。
-
模板参数还可以是 “非类型模板参数(普通的参数)”
-
c++17开始,支持非类型模板参数为auto类型。
指定非类型模板参数的值时,一般给的都是常量。
因为编译器在编译的时候就要能够确定非类型模板参数的值。并不是任何类型的参数都可以作为非类型模板参数。
int类型可以,但double,float或者类类型string等等可能就不可以,*不过double 这种指针类型可以。
一般允许做非类型模板参数的类型如下:可能不断增加
- 整型或者枚举型;
- 指针类型;
- 左值引用类型;
- auto或者decltype(auto);
- 可能还有其他类型,请自行总结;
namespace _nmsp2 { template < typename T, typename U ,int val = 100> // 非类型模板参数 int val=100 //template < typename T, typename U, auto val = 100> // auto C++17 auto Add(T tv1,U tv2) { return tv1 + tv2 + val; } //------------------- template <double *p> void mft() { cout << "mft()执行了" << endl; } double g_d = 1.3; //全局量 //------------------- //template <typename T,int value> //template <typename, int> template <typename T, typename int value> auto Add2() { return 100; } } int main() { cout << _nmsp2::Add<float, float>(22.3f, 11.8f) << endl; // 134.1 cout << _nmsp2::Add<float, float, 800>(22.3f, 11.8f) << endl; // 834.1 return 0; }
-
-
比较奇怪的语法
- 不管类型还是非类型模板参数,如果代码中没有用到,则参数名可以省略。
- 类型前面可以增加一个
typename
修饰以明确标识一个类型,一般跟模板有关的类型名前面是需要typename
namespace _nmsp2
{
//template <typename T,int value>
//template <typename, int>
template <typename T, typename int value> // 第一个typename 修饰类型模板参数,第二个 // typename表示后面修饰一个类型
auto Add2()
{
return 100;
}
}