模板与泛型编程

写在前面

面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。

模板是泛型编程的基础。我们不必了解模板是如何定义的就能使用它们,例我们用过的vector。

定义

模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。

模版可以分为两类,一个是函数模版,另外一个是类模版。

我们什么时候考虑使用模板技术呢?
假定我们希望编写一个函数来比较两个值,并指出第一个值是小于、等于还是大于第二个值。在实际中,我们可能想要定义多个函数,每个函数比较一种给定类型的值。例:尝试定义多个重载函数

//如果两个const string&类型的值相等,返回0,如果v1 小返回-1,如果v2小返回1
int compare(const string & v1, const string &v2)
{
	if (v1 < v2) return -1;
	else if (v2 < v1) return 1;
	return 0;
}

//下面重载是是double类型的
int compare(const double& v1, const double& v2)
{
	if (v1 < v2) return -1;
	else if (v2 < v1) return 1;
	return 0;
}

可以发现这两个函数几乎是相同的,唯一的差异是参数的类型,函数体则完全一样。

因此当发现有上述情况时,就可以考虑使用函数模板了。

函数模板

首先看一下函数模板的定义:在定义函数时,函数的类型和函数的参数类型不具体指定,而用一个虚拟的类型来代替,实际上是建立一个通用的函数,称这样的函数为函数模板。

函数模板以关键字template开始,后跟一个模板参数列表(template parameter list),这是一个以逗号分隔的一个或多个**模板参数(template parameter)**的列表,用<> 包围起来。例:

template <typename T>
int compare(const T& v1, const T& v2)
{
	if (v1 < v2) return -1;
	else if (v2 < v1) return 1;
	return 0;
}

注意:在模板定义中,模板参数列表不能为空。

//错误的定义
template <>
void func()
{
	//一些实现
}

模板参数列表的作用很想函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时调用者提供实参来初始化形参。

类似的,模板参数表示在类或函数定义中用到的类型或值,当使用模板时,我们(隐式的或显式地)指定模板实参(template argument),将其绑定到模板参数上。

例上面的compare函数声明了一个名为T的类型参数。在compare中,我们用名字T表示一个类型。而T表示的实际类型在编译时根据compare的使用情况来确定。

实例化函数模板

当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实现。例:

cout << compare(1, 0) << endl;
//通过compare的函数实参1, 0推断处T 为int,并将其绑定到模板参数T。

编译器用推断出的模板参数来为我们**实例化(instantiate)**一个特定版本的函数。例:

//实例化出int compare(const int& , const int&)
cout << compare(1, 0) << endl;	//T 为 int
//实例化出int compare(const vector<int>&, const vector<int>&)
vector<int> v1{1, 2, 3, 4}, v2{5, 6, 7};
cout << compare(v1, v2) << endl;	//T 为 vector<int>

编译器会实例化出两个不同版本的compare。对于第一个调用【compare(1, 0)】,编译器会编写并编译一个compare版本,其中T被替换为int。如下:

int compare(const int& v1, const int& v2)
{
	if (v1 < v2) return -1;
	else if (v2 < v2) return 1;
	return 0;
}

同理,对于第二个调用【compare(v1, v2)】,编译器会生成另一个compare版本,其中T被替换为vector。

这些编译器生成的版本通常被称为模板 的实例(instantiation)

模板类型参数

我们的compare函数有一个模板类型参数(type parameter)(即上面的T)。类型参数前必须使用关键字class 或 typename

在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表张可以同时使用这两个关键字。例:

//正确用法
template <typename T, class U>
void calc(const T&, const U&);

//错误用法:类型参数列表中的U没有加关键字
template<typename T, U>
void calc(const T&, const U&);

非类型模板参数

除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter)

一个非类型参数表示一个值而非一个类型,通过一个特定的类型名(而非关键字class 或 typename)来指定非类型参数。

当一个模板被实例化(即调用函数模板)时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式(constexpr),从而允许编译器在编译时实例化模板。

例:

//处理字符串字面常量的模板
//我们希望能比较不同长度的字符串字面常量,因此模板定义了两个非类型参数:第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
	return strcmp(p1, p2);
}

//当我们调用这个版本的模板时:compare("hi", "mom");
//编译器会根据实参推断出N = 3,M = 4。
//为什么不是2和3呢? 因为我们的实参是string类型的,string转char* 会在末尾插入一个空字符('\0')作为终止符。因此编译器会实例化出如下版本:int compare(const char (&p1)[3], const char (&p2)[4]);

一个非类型参数可以是一个整型,也可以是一个指向对象或函数的指针或引用。

需要注意的是绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。例:

int a = 3;
int b = 4;
//调用处理字符串字面常量的模板
//错误调用
compare<a, b>("hi", "mom");	//这里显示指定模板实参为3, 4。但这里是一个错误的调用。因为变量a和变量b不是常量表达式

//下面为正确调用
const unsigned int c = 3;	//c是常量表达式
const unsigned int d = 4;	//d也是常量表达式
compare<c, d>("hi", "mom");	//正确调用

inline 和 constexpr的函数模板

函数模板可以声明为inline 或 constexpr的,如同非模板函数一样。inline 或 constexpr说明符放在模板参数列表之后,返回类型之前:

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

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

编写类型无关的代码

我们最初的compare函数虽然简单,但它说明了编写泛型代码的两个重要原则:

  • 模板中的函数参数是const的引用。
  • 函数体中的条件判断仅使用<比较运算。

将函数参数设定为const&,保证了函数可以用于不能拷贝的类型(例拷贝构造函数和赋值拷贝构造被删除的类型),且使得compare在处理大对象是使函数允许的更快。

为什么只使用<运算符而不是< 和 > 一起使用?

因为只使用<运算符,有效的降低了compare函数对要处理的类型的要求:这些类型必须支持<, 但不必同时支持>。例某个类型重载了<运算符而没有重载>运算符。

所以,模板程序应该尽量减少对实参类型的要求

模板编译

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

//编译到如下定义时,并不生成代码
template <typename T>
int compare(const T& v1, const T& v2)
{
	if (v1 < v2) return -1;
	else if (v2 < v1) return 1;
	return 0;
}

//编译到如下语句时,通过模板实参(1, 0)推断出模板类型T 为int,然后生成该版本的实例
cout << compare(1, 0) << endl;	//T 为 int

为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,因此模板的头文件中通常既包括声明也包括实现。

总结

当调用一个函数模板时,编译器会根据给定的函数实参来推断模板实参,并以此实际类型参数代替模板参数来创建出模板的一个新的实例,也就是一个真正可以调用的函数。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值