函数模板
不知在你刚学C++
编程时候,是否有过这样的困惑?一个函数的实现除了类型不一致外,函数内部实现完全一样,代码给人一种冗余的感觉。看下面的函数示例:
// int 类型实现两个数相加
int Add(int a, int b)
{
return a + b;
}
// float 类型实现两个数相加
float Add(float a, float b)
{
return a + b;
}
上述的Add()
函数就是一个典型的函数代码冗余的例子,函数内部实现完全一样,除了函数的形参与返回值的类型不一致。这个时候,通过模板方式的编程能够降低冗余。请看下面的Add()
函数模板代码:
template <typename T>
T Add(T a, T b)
{
return a + b;
}
上述的Add()
函数模板就能够代替上面的两种(int与float
)不同类型的Add()
实现方式。函数模板实例化只在该函数模板被调用情况下才会生成(注意这里并不是运行时确定),类型推断是在编译阶段来进行确定下来。
定义与基本使用
我们先看一下函数模板的基本定义,如下所示:
//template <typename 类型参数1,typename 类型参数2,...>
template <typename T1, typename T2, ...>
//返回值类型 模板名 (形参表)
T1 Add(T1 a, T2 b)// ...代表是否函数形参有其它的类型,没有的话可以去除
{
// 函数体
};
- typename后面的类型参数可以定义一个,也可以定义多个,主要依据函数体实现需要;
- 返回值类型并不一定为T1,可以是函数所需要的类型,也可以无返回void类型,与基本函数使用一致;
我们声明与实现好了函数模板,以Add()
函数模板为例,我们如何调用此函数模板,代码如下:
#include <iostream>
using namespace std;
template <typename T>
T Add(T a, T b)
{
return a + b;
}
// 如果存在函数Add,那么当main函数中调用,①优先使用普通函数
int Add(int a, int b)
{
return a + b;
}
int main(void)
{
cout << Add(3, 5) << endl; // ① 输出结果:8
cout << Add<>(3, 5) << endl; // ② 输出结果:8
cout << Add<int>(3.2, 5.1) << endl; // ③ 输出结果:8
cout << Add<double>(3.2, 5.1) << endl; // ④ 输出结果:8.3
return 0;
}
下面我们根据上面的代码进行简要的分析:
- ①
Add(3, 5)
会根据输入的数值来进行推导T的类型为int; - ②
Add<>(3, 5)
这里与①效果一样,默认不使用<>
,编译期间推导类型int; - ③
Add<int>(3.2, 5.1)
显示声明T的类型为int,虽然输入是浮点数,数值经过隐式转换; - ④
Add<double>(3.2, 5.1)
显示声明T的类型为double;
上述的①与②在函数模板中没有显示的构造函数时候,没有任何区别。如果上述代码存在普通int
型函数Add()
,那么①优先使用普通函数,如果不存在,那么①会通过函数模板实例化int
型的Add()
函数。只是②这种写法会显示调用函数模板,③与④都是显示对函数模板参数类型实例化。当然函数模板也支持①或者②这种隐式实例化模式。
函数模板的重载
函数模板重载,只需它们的形参表
或者类型参数表
不同。
#include <iostream>
using namespace std;
template <typename T, typename U>
void print(T a, U b)
{
cout << "template <typename T, typename U> print(T a, U b): " << a << " " << b << endl;
}
template <typename T>
void print(T a, T b)
{
cout << "template <typename T> print(T a, T b): " << a << " " << b << endl;
}
template <typename T, typename U>
void print(T a, T b)
{
cout << "template <typename T, typename U> print(T a, T b): " << a << " " << b << endl;
}
int main(void)
{
print<int, double>(2, 1.2); // 第一个模板函数
print<double>(2.1, 1.2); // 第二个模板函数
print<int, double>(3, 6); // 第三个模板函数
return 0;
}
特化
特化与泛化的概念
- 特化:一般情况下是指泛化版本的子集;
- 泛化:常规声明的函数模板一般都是泛化的函数模板;
函数模板主要不存在全特化与偏特化的方式,一般通过函数重载方式来进行偏特化的实现。通过模板参数数量上的变化来实现函数模板偏特化的方式并不可行,语法上不允许。下面的示例程序主要介绍函数模板全特化与函数重载的方式实现偏特化。
- 全特化:所有的函数模板参数都用具体类型来进行替换,同时
template<>
不带参数; - 偏特化:通过函数重载的方式进行实现;另外一种是模板参数类型的变化;
#include <iostream>
using namespace std;
// 泛化函数模板
template <typename T, typename U>
void printMsg(T t, U u)
{
cout << "print(T t, U u)泛化版本被调用." << t << ", " << u << endl;
}
// 全特化函数模板
template <>
void printMsg(int t, float u)
{
cout << "print(int t, float u)全特化版本被调用." << t << ", " << u << endl;
}
// 函数模板重载来实现偏特化方式
template <typename U>
//void printMsg<double, U>(double t, U u) // error,不能从模板参数数量上进行偏特化
void printMsg(double t, U u)
{
cout << "print(T t, double u)重载版本被调用." << t << ", " << u << endl;
}
int main(void)
{
// 调用泛化函数模板
printMsg(2, 8);
// 调用偏特化函数重载方式
printMsg<double>(5.6, 8.7);
// 调用全特化函数模板
printMsg<int, float>(2, 8.7);
return 0;
}
带有缺省参数的函数模板
带有缺省参数的函数模板,第一缺省参数可以在前面声明,第二如果没有指定缺省参数的类型,那么函数生成模板就会按照缺省函数模板的进行生成。
#include <iostream>
using namespace std;
template <typename T = double, typename U>
void printMsg(U b)
{
T value = b;
cout << "value : " << b << endl;
}
int main(void)
{
// 函数第一个参数T默认double,第二个参数通过推导得出U为double
// 因此:函数模板实例化为printMsg<double, double>
printMsg(1.8);
return 0;
}
非类型模板参数
指定非类型模板参数的值一般都是常量。具体使用方法见下面代码:
#include <iostream>
using namespace std;
template <typename T, size_t u = 10>
void printMsg(T b)
{
T value = b + u;
cout << "value : " << value << endl;
}
int main(void)
{
// 函数模板中的u为非类型模板参数,编译期间就已经确认大小
printMsg<double, 100>(1.8);
// 输出值与printMsg<double, 100>第二个参数有关,默认值为10
return 0;
}
函数模板的调用次序
- 优先寻找参数完全匹配的普通函数(非由模板实例化而得的函数);
- 其次再找参数完全匹配的模板函数;
- 再找实参数经过自动类型转换后能够匹配的普通函数;
小结
函数模板主要为了解决函数实现基本一致,函数参数不同导致的重复性代码实现。为了降低冗余而产生的泛型函数模板编程,将函数模板实例化的任务交给编译器来进行解决。函数模板使用有着严格的类型匹配要求,这点与普通函数会存在类型隐式转换不同。对于函数模板,函数特化,普通函数被编译器调用书序依次为:普通函数、函数特化、函数模板(实参进行转换后的普通函数)。对于函数模板实例化时候是否使用<>
代表是否显示调用函数模板。