主页
文章
分类
系列
标签
简历
【模板与泛型编程01】函数模板
发布于: 2022-6-8   更新于: 2022-6-8   收录于: Cpp
文章字数: 595   阅读时间: 3 分钟   阅读量:

一、定义函数模板

1、基本范例

1
2
3
4
5
6
7
template <typename T>
int compare(const T &v1 , const T &v2)
{
	if(v1 < v2) return -1;
	if(v2 < v1) return  1;
	return 0;
}

模板定义以关键字 template开始,后接模板形参表,模板形参表是用尖括号<>括住的一个或多个模板形参的列表,用逗号分隔,不能为空

  • 模板程序应该尽量减少对实参类型的要求。
  • 函数模板和类模板成员函数的定义通常放在头文件中。

2、模板参数

2.1 类型参数

类型参数前必须使用关键字class或者typename,这两个关键字含义相同,可以互换使用。旧的程序只能使用class。但是有些时候,class并不合适

2.2 默认参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T , typename F = FunType> void TestFunc(T i , T j , F funcpoint=mf)
{
	cout<<funcpoint(i,j)<<endl;
}

int main()
{
	TestFunc(15,16);
	return 0;
}

调用Testfunc()函数的时候,不用指定第3个实参,因为第3个参数有默认值。要注意默认参数的写法:针对当前的范例,类型模板参数F给了默认值,函数的形参也给了默认值。默认模板参数F是一个函数指针类型(FuncType),函数参数funcpoint = mf中的mf是函数名,代表函数首地址

另外,函数模板的默认模板参数可以放在前面(这一点类模板默认模板参数不一样,类模板的模板参数一旦有一个是默认参数,则其后续的参数都需要是默认参数)。

2.3 非类型参数

除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter) 表示一个值而非一个类型。 当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达,从而允许编译器在编译时实例化模板。

1
2
3
4
5
6
7
8
9
template<unsigned N , unsigned M>
int compare(const char (&p1)[N] , const char (&p2)[M])
{
	return strcmp(p1,p2);
}

compare("hi" , "mom");
//实例化模板
//int compare(const char (&p1)[3] , const char (&p2)[4])

一个非类型参数可以是一个整形,或者是一个指向对象或函数类型的指针或引用,绑定到非类型整数参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参,必须具有静态的生存期。

但是,并不是任何类型的参数都可以作为非类型模板参数,一般有以下一些是允许的 (1)整型或枚举类型。 (2)指针类型。 (3)左值引用类型。 (4)auto或decltype(auto)。对于decltype(auto)这个用法,其中的auto理解成要推导的类型,而decltype理解成推导过程采用decltype推导。 (5)可能还有其他类型,请读者自行在学习或阅读他人代码的过程中收集和总结。

二、 实例化函数模板

这里可以给实例化一个定义:用具体的“类型”代替“类型模板参数”的过程就叫作实例化(也称为代码生成器)。

1、一个错误的实例化示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template <typename T>
T sub(T tv1 , T tv2)
{
	return tv1 - tv2;
}

int main()
{
	int    v1 = 1;
	int    v2 = 2;
	sub(v1,v2);//return -1
	string s1 = "Hello";
	string s2 = "World";
	sub(s1,s2);//报错
	return 0;
}

所以,同样一个函数模板,可能以某种方式进行调用是合法的,而换一种方式调用就不合法了。尤其值得注意的是,这种合法性,在==编译阶段就可以由编译器判断出来==,因为这些对Sub()函数模板的调用代码就在这里摆着,编译器有能力在编译时就从这些调用代码中去推断Sub()函数模板中的模板参数T的类型。根据模板参数T的类型,编译器就能够判断出这个类型是否支持减法运算。

2、编译器视角的实例化

1
2
sub(1,2);
sub(1.1 , 2.2);
1
2
//.obj
int __cdecl sub<int>(int,int) double __cdecl sub< double >( double, double)

这说明在编译阶段,在对模板进行具体针对某类型的实例化之前,编译器需要查看函数模板的函数体,确定能否针对该类型进行实例化

当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码

当编译器遇到类和普通函数

  1. 普通函数 当我们调用一个函数时,编译器只需要掌握函数的声明
  2. 类 当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现 我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中

为了生成个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义,所以函数模板与类模板成员函数的定义通常放在头文件中

3、模板参数的实例化

1
2
3
4
5
template<typename T> 
T mydouble(T tmpvalue) 
{ 
	return tmpvalue * 2; 
}

3.1 显示实例化

1
2
int result = mydouble<int>(16);
int result = mydouble<int>(17.7);//warning C4244: “参数”: 从“double”转换到“T”,可能丢失数据

3.2 隐式实例化

1
2
3
cout << compare(1,0) << endl;
//实例化出一个特别版本的函数
//compare(const int& , const int&);

编译器用函数实参来为我们推断模板实参,实参类型是int。编译器会推断出模板实参为int , 并将它绑定到模板参数T。 编译器用推断出的模板参数来为我们实例化(instantiate) 一个特定版本的函数

隐式实例化下的空参数列表

1
auto result = mydouble<>(16.9);

< >的作用:< >没什么用处,但是当有一个也叫作mydouble()的普通函数存在时,< >也许就会发挥作用

3.3 部分实例化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename V,typename T,typename U> 
V Add(T tv1, U tv2) 
{ 
	return tv1 + tv2; 
}

int main()
{
	Add(15,17.8);//error C2672: “Add”": 找到匹配的重载函数 error C2783: “V Add(T,U)”: 未能为“V”推导模板参数
	Add<double>(1.1 , 2); // return 3.1
	return 0;
}

通过尖括号指定一部分模板参数,另外一部分模板参数可以让编译器去推断。但是,一旦从某个模板参数开始推断,后续的所有模板参数都需要让编译器推断,==不可以自己指定第1个类型V和第3个类型U,然后推断中间第2个类型T,编译器不支持这种语法。==

3.4 特化

1
2
3
4
5
6
7
template <typename T, typename U> 
void tfunc(T& tmprv, U& tmprv2) 
{ 
	cout << "tfunc泛化版本" << endl; 
	cout << tmprv << endl; 
	cout << tmprv2 << endl; 
}

3.4.1 全特化

所谓全特化,就是把tfunc()这个泛化版本中的所有模板参数都用具体的类型代替,构成一个特殊的版本(全特化版本),既然所有模板参数都用具体的类型代替了,那么tfunc()泛化版本中template后面尖括号中的内容就变成空了。

1
2
3
4
5
template<>
void tfunc<int , double>(int& tmprv, double& tmprv2)
{
	cout<<"tfunc特化"<<endl;
}

全特化实际上等价于实例化一个函数模板,并不等价于一个函数重载

1
2
3
4
void tfunc(int& tmprv, double& tmprv2)
{
	cout<<"tfunc函数重载"<<endl;
}

==调用优先级:普通函数 > 函数模板特化 > 函数模板泛化==

3.4.2 偏特化

(1)模板参数数量上的偏特化

What is : 特化第1个模板参数类型为double类型,但第2个模板参数不特化

实际上,从模板参数数量上来讲,函数模板不能偏特化,只有类模板才能偏特化

(2)模板参数范围上的偏特化

What is : 所谓“参数范围”,比如原来是int类型,如果变成const int类型,那么与int类型相比,const int类型的范围就变小了;再比如,如果原来是任意类型T,现在变成T *(从任意类型缩小为任意指针类型),那这个类型的范围也是变小了;还有T &(左值引用)、T&&(右值引用),对于T,从类型范围上都属于变小了。

对于函数模板,也不存在模板参数范围上的偏特化。因为这种所谓模板参数范围上的偏特化,实际上是函数模板的重载

1
2
3
4
5
template <typename T , typename U>
void tfunc(const T& tmprv1, U& tmprv2)
{
	cout<<"函数模板的参数范围偏特化本质上是函数模板的重载"<<endl;
}

3.5 实例化模板的返回值问题

  • 显示实例化
1
Add<double>(1.1 , 3);
  • auto
  • 使用auto结合decltype完成返回值类型推断
1
2
3
4
5
6
template <typename V,typename T,typename U> 
auto Add(T tv1, U tv2) 
//auto Add(T tv1, U tv2) -> decltype(tv1 + tv2)
{ 
	return tv1 + tv2; 
}

三、特异的语法

1、省略参数

不管是类型模板参数还是非类型模板参数,如果在代码中没有用到这个参数,则参数名可以省略

1
2
3
template <typename T ,int value>
auto Add2()
{return 100;}

可以做如下省略

1
2
3
template <typename ,int>
auto Add2()
{return 100;}

2、“无用"的typename

类型前面可以增加一个typename修饰以明确标识一个类型。有的时候为了表明其后面是一个类型,也需要用typename修饰

1
2
3
4
5
template <typename T , typename int value>
auto Add()
{
	return 100;
}

四、模板函数在工程中

1、inline和constexpr的含函数模板

inline或 constexpr说明符放在模板参数列表之后,返回类型之前:

1
2
template <typename T> inline T min(const T&, const T&);
template <typename T> constexpr T min(const T& , const T&);

2、编写与类型无关的代码

1
2
3
4
5
6
template <typename T> int compare(const T &v1 , const T &v2)
{
	if(less<T>()(v1,v2)) return -1;
	if(less<T>()(v2,v1)) return  1;
	return                       0;
}

这样写的好处

  1. const T &作为函数参数可以避免实参是不可调用类型
  2. less<T>()使用标准库可以避免有些类型没有定义>的比较