C++ 模版基础知识——函数模板

C++ 模版基础知识——函数模板

1. 基本范例

以下是一个函数模板的典型范例,因为函数参数类型未确定,所以使用 T 来表示,这个 T 称为模板参数,更准确地说,是一个类型模板参数,因为它代表一个类型。

#include <iostream>
using namespace std;

template <typename T>
T Sub(T tv1, T tv2)
{
    return tv1 - tv2;
}

int main()
{
    int subv = Sub(3, 5);
    cout << "subv = " << subv << endl; // subv = -2
    double subv2 =Sub(4.7 , 2.1);
    cout << "subv2 = " << subv2 << endl; // subv2 = 2.6
    
    return 0;
}
关于模板的基础知识
  • 模板定义以 template 关键字开始。
  • 类型模板参数 T 前面用 typename 修饰(这是语法规定),因此,看到 typename 就知道后面跟的是一个类型。这里的 typename 可以用 class 替换(但推荐使用 typename),这里的 class 并没有“类”的意思(有些人习惯于使用typename 表明对应的模板实参可以是任意类型,而使用class 表明对应的模板实参必须是一个类类型)。
  • 类型模板参数 T(代表一个类型)以及前面的修饰符 typename 都用尖括号 <> 括起来。
  • T 可以替换为任何其他标识符,对程序没有影响,使用 T 只是一种习惯。

2. 实例化

基本示例代码分别用 intdouble 类型的参数调用了 Sub() 函数模板。由于存在这种调用代码,编译器在编译时会对此函数模板进行实例化。实例化可以定义为一个过程:用具体的“类型”代替“类型模板参数”。

对于 intdouble 类型,每种类型都会被实例化一次,因此,Sub() 函数模板总共会被实例化两次,相当于生成了两个 Sub() 函数。

使用开发人员命令行工具和 dumpbin 命令行工具将 .obj 文件转换为 .txt 文件。在 .txt 文件中,使用 Ctrl+F 快捷键并查找“Sub”字样,可以看到以下结果。

int __cdecl Sub<int>(int,int) 
double __cdecl Sub< double >( double, double)

可以看出,实例化后的函数名分别为 Sub<int>Sub<double>,这与普通函数名不同。实例化后的函数名由三部分组成:模板名、一对尖括号 <> 和具体类型。显然,编译器为 Sub() 函数模板实例化出了两个具体的 Sub() 函数,这两个函数的区别在于它们的参数和返回值类型。

结论:编译器根据程序员调用函数模板的参数类型,智能地决定如何实例化函数模板。

3. 模版参数的推断

3.1 常规的参数推断
1. 明确指定类型

在这个例子中,明确指定了 TUV 的类型,分别为 intdoubledouble

template <typename T,typename U,typename V> 
V Add(T tv1, U tv2) 
{ 
    return tv1 + tv2; 
}
cout << Add(15, 17.8) << endl;
cout << Add<int, double, double>(15, 17.8) << endl;
2. 返回类型指定为 auto

在这里,使用 auto 关键字来推断返回类型。编译器会根据 tv1 + tv2 的结果类型来确定 Add 函数的返回类型。

template <typename T, typename U > 
auto Add(T tv1, U tv2) 
{ 
    return tv1 + tv2;
}

cout << Add<double>(15, 17.8) << endl;
3. 使用 autodecltype 推断返回类型

这种写法中,auto 并不参与类型推导,它只是构成“返回类型后置语法”的一部分。真正进行类型推导的是 decltype 关键字,它根据 tv1 + tv2 的表达式类型来确定返回类型。

template <typename T, typename U > 
auto Add(T tv1, U tv2) -> decltype(tv1 + tv2)
{ 
    return tv1 + tv2;
}
3.2 空模板参数列表的推断

再看一个范例,定义一个名为mydouble的函数模板如下。

template<typename T> 
T mydouble(T tmpvalue) 
{ 
    return tmpvalue * 2; 
}

对上述mydouble()函数模板的调用,可以有很多种方式。

1)自动推断指定实参,编译器会自动推断出模板参数的类型,调用代码如下。

int result = mydouble(15); // 推断出 T 为 int

2)指定类型模板参数同时指定实参和类型模板参数,调用代码如下。

int result2 = mydouble<int>(16.9); // 显式指定 T 为 int

3)指定空模板参数列表,调用代码如下。

auto result3 = mydouble<>(16.9); // 推断出 T 为 double

4)空模板参数列表 <> 用于指定调用函数模板,即使有一个同名的普通函数存在,调用代码如下。

double mydouble(double tmpvalue)
{
    return tmpvalue * 2;
}

auto result4 = mydouble(16.9); // 调用普通函数
auto result3 = mydouble<>(16.9); // 调用函数模板

所以,<>这个空模板参数列表的用处是可以明确地指出:请调用mydouble()函数模板,而不是调用普通的mydouble()函数。

结论:在函数模板的调用中,空模板参数列表 <> 可以用来明确调用函数模板,即使存在同名的普通函数。

4. 重载

函数模板的重载与函数的重载比较类似,函数(函数模板)重载的概念是:函数(函数模板)名字相同,但参数数量或参数类型不同。编译器会根据调用时给出的具体实参,选择一个编译器认为最合适的函数模板实例化和调用。

看一个简单的范例,写一个函数模板myfunc(),注意其形参的形式。

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;
}

myfunc(12);   // myfunc(int tmpvalue)执行了
char* p = nullptr;
myfunc(p);    // myfunc(T* tmpvalue)执行了
myfunc(12.1); //myfunc(T tmpvalue)执行了

5. 特化

特化函数模板是泛化函数模板的一个特定版本,针对特定的类型参数进行了优化。泛化函数模板指的是能够处理多种类型参数的模板,通常适用于各种类型的输入。以下是一个泛化函数模板的例子:

#include <iostream>
using namespace std;

template <typename T, typename U> 
void tfunc(T& tmprv, U& tmprv2)
{ 
    cout << "tfunc泛化版本" << endl;
    cout << tmprv << endl;
    cout << tmprv2 << endl;
}

int main()
{
	const char* p = "I Love China!"; 
	int i = 12; 
	tfunc(p, i); // tfunc泛化版本 I Love China! 12
}

在这个例子中,tfunc 函数模板接受两个参数,它们的类型分别为 TU。执行 tfunc(p, i); 时,会实例化 tfunc<const char*, int>

1. 全特化

全特化是将泛化版本中的所有模板参数用具体类型替代,形成一个特殊的版本。以下是一个全特化的例子:

template<> //全特化<>中就是空的
// 替换原来的T,U,格式要与泛化版本一一对应,否则编译就报错,
// 例如第2个参数写成double tmprv2就会报错,
// tfunc后面的<int,double>可以省略,因为根据实参可以推导出T和U的类型
void tfunc<int,double>(int& tmprv,double& tmprv2) 
{
    cout << "tfunc<int, double>特化版本" << endl;
    cout << tmprv <<endl;
    cout << tmprv2 << endl;
}

int k = 12;
double db = 15.8; 
tfunc(k, db); //这里调用的是特化版本

运行结果

tfunc<int, double>特化版本
12
15.8

全特化实际上等价于实例化一个函数模板,并不等价于一个函数重载,注意比较下面两行代码:

void tfunc<int, double>(int& tmprv, double& tmprv2){...}; //全特化,等价于实例化一个函数模板 
void tfunc(int& tmprv, double& tmprv2){...} //重载的函数

如果存在一个与特化版本参数相同的普通函数:

void tfunc(int& tmprv, double& tmprv2) 
{ 
    cout << "tfunc普通函数" << endl; 
}

在这种情况下,调用 tfunc(k, db); 将会调用普通函数,而不是特化版本。运行结果如下:

tfunc普通函数

这说明,当编译器在选择函数时,优先选择普通函数;如果没有合适的普通函数,才考虑函数模板的特化版本;如果没有特化版本或特化版本不匹配,最后才考虑泛化版本。

2. 偏特化(局部特化)

偏特化是指对模板参数的部分特化。全特化是将所有模板参数替换为具体类型,而偏特化则只针对某些模板参数进行特化。

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

函数模板本身并不支持数量上的偏特化,只有类模板支持。例如,以下代码将会编译错误:

//函数模板不能偏特化,下列代码编译报错 
template<typename U> 
void tfunc<double, U>(double& tmprv, U& tmprv2) //注意特化版本tfunc后面要有尖括号 
{ 
	cout <<"tfunc<double,U>偏特化版本" << endl; 
	cout << tmprv << endl; 
	cout <<tmprv2 << endl; 
}

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

参数范围的偏特化是指将类型范围缩小,比如将 int 替换为 const int 或将 T 替换为 T*。这通常是通过函数模板的重载实现的,而不是偏特化。

template<typename T, typename U>
void tfunc(const T& tmprv, const U& tmprv2)
{
    cout <<"tfunc(const T& tmprv, const U& tmprv2)重载版本" << endl;
    cout << tmprv << endl;
    cout <<tmprv2 << endl;
}

const int k2 = 12;
const double db = 15.8;
tfunc(k2, db); 

运行结果:

tfunc(const T& tmprv, const U& tmprv2)重载版本
12
15.8

3)通过重载实现模板参数数量上的偏特化

可以通过重载的方式模拟偏特化的效果。例如,支持第一个模板参数为 doubletfunc() 函数模板的重载:

template<typename U>
void tfunc(double& tmprv, U& tmprv2) //注意特化版本tfunc后面要有尖括号
{
    cout <<"tfunc<double,U>偏特化的tfunc重载版本" << endl;
    cout << tmprv << endl;
    cout <<tmprv2 << endl;
}

int main()
{
    const char* p = "I Love China!";
    int i = 12;
    tfunc(p, i);
    double j = 18.5;
    tfunc(j, i);

    return 0;
}

运行结果:

tfunc泛化版本
I Love China!
12
tfunc<double,U>偏特化的tfunc重载版本
18.5
12

6. 默认参数

函数模板可以提供默认的模板参数,下面举一个简单的例子。

#include <iostream>
using namespace std;

// 普通函数
int mf(int tmp1, int tmp2)
{
    return tmp1 + tmp2;
}

// 函数指针类型定义
typedef int(*FunType)(int, int);

// 模板函数
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); // 31
    return 0;
}

在这个例子中,调用 testfunc() 函数时,不需要指定第三个实参,因为该参数有默认值。

需要注意的是,默认参数的写法:在本例中,模板参数 F 被赋予了默认值 FunType,而函数参数 funcpoint 的默认值是 mf,这是一个普通函数的名称,代表了函数的首地址。

如果函数模板的形参没有默认值,则在调用时必须显式指定该参数,例如:

testfunc(15, 16, mf);

另外,函数模板的默认模板参数可以放在前面

template <typename T=int, typename U>
void testfunc2(U m)
{
    T tmpvalue =m;
    cout << tmpvalue << endl;
}

testfunc2(15); // 15

7. 非类型模板参数

非类型模板参数主要还是用于传递一些数组大小之类的信息,目的是在提高程序运行效率等方面与C语言的数组进行竞争。另外,如果能换成普通的函数参数解决问题,还是优先考虑使用普通的函数参数,而不是非类型模板参数。

非类型模板参数的声明方式如下:

template <typename T, int N>

实际上,模板参数不但可以是一个类型,也可以是一个普通的参数(非类型模板参数),当然,这种普通参数也可以有一个默认值。

下面的代码中,在template后的尖括号中,增加了一个普通的int类型的参数,并且给了一个默认值100

template <typename T, typename U, int val = 100>
auto Add(T tv1, U tv2)
{
    return tv1 + tv2 + val;
}

int main()
{
    cout<<Add<float,float>(22.3f,11.8f)<<endl;    //134.1
    cout<<Add<float,float,800>(22.3f,11.8f)<<endl; //834.1
    return 0;
}

在 C++17 中,auto 也可以作为非类型模板参数,这为模板参数的类型提供了更大的灵活性。

template <typename T, typename U, auto val = 100>
auto Add(T tv1, U tv2)
{
    return tv1 + tv2 + val;
}

需要注意的是,并非所有的类型都可以作为非类型模板参数,如 floatdouble 类型等就无法作为非类型模板参数。目前,可以作为非类型模板参数的类型主要有以下几种:

  1. 整型或枚举类型。
  2. 指针类型。
  3. 左值引用类型。
  4. autodecltype(auto)

8. 总结

C++ 函数模板是实现代码重用和类型安全的重要工具。通过使用模板,开发者能够编写通用的函数,这些函数可以处理多种数据类型,而无需为每种类型编写重复的代码。这种灵活性使得 C++ 在泛型编程方面具有显著优势。

  1. 代码重用性:函数模板允许开发者编写一次代码,并在多种数据类型上使用。这种机制不仅减少了代码量,还降低了维护成本。
  2. 类型安全:模板在编译时进行类型检查,确保类型一致性,避免了运行时错误。编译器会根据调用时的参数类型实例化相应的函数版本,从而提供了类型安全的保障。
  3. 实例化与推断:模板的实例化过程使得编译器能够生成特定类型的函数。参数推断机制进一步简化了函数调用,开发者可以在不显式指定模板参数的情况下使用模板。
  4. 重载与特化:函数模板支持重载,允许同名函数根据参数类型和数量的不同进行选择。此外,通过全特化和偏特化,开发者可以针对特定类型提供优化的实现,提高了程序的性能和灵活性。
  5. 默认参数的灵活性:模板函数可以定义默认参数,让函数调用更加灵活。开发者可以选择性地提供参数,简化函数调用的复杂性。
  6. 非类型模板参数:引入非类型模板参数后,开发者可以在模板中使用常量值,如数组大小。这种特性使得模板更加灵活,并能够在编译时进行更多的优化。
  7. 广泛应用:函数模板在 STL(标准模板库)中得到了广泛应用,开发者可以利用模板编写各种数据结构和算法,如容器、迭代器和算法函数等。
  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值