函数模板
本节阅读量:假设您想编写一个函数来计算两个数字的最大值。您可以这样做:
|
|
虽然调用方可以将不同的值传递到函数中,但参数的类型是固定的,因此调用方只能传入int值。这意味着该函数实际上只适用于整数(以及可以提升为int的类型)。
那么,当您想找到两个double值的最大值时,会发生什么呢?由于C++要求我们指定所有函数参数的类型,因此解决方案是创建一个新的重载版本的max,参数类型为double:
|
|
注意,这个版本max的实现代码与int版本max完全相同!事实上,这个实现适用于许多不同的类型:包括int、double、long、long double,甚至是您自己创建的新类型(我们将在以后的课程中介绍如何做)。
为想要支持的每一组参数类型创建具有相同实现的重载函数,这是一个维护难题,是错误的方案,并且明显违反了DRY(不要重复自己)原则。这里还有一个不太明显的挑战:希望使用max函数的程序员可能以max函数编写者没有预料到的参数类型来调用它(因此没有为此编写重载函数)。
我们真正缺少的是某种编写max的单个版本的方法,该版本可以与任何类型的参数一起工作(即使是在编写max代码时可能没有预料到的类型)。正常的函数根本不能胜任这里的任务。幸运的是,C++支持另一个专门为解决这种问题而设计的功能。
欢迎来到C++模板的世界。
C++模板简介
在C++中,模板系统旨在简化创建能够处理不同数据类型的函数(或类)的过程。
我们不是手动创建一组基本相同的函数或类(每组不同类型一个),而是创建一个模板。就像普通定义一样,模板描述函数或类的外观。与普通定义(其中必须指定所有类型)不同,在模板中,我们可以使用一个或多个占位符类型。占位符类型表示在编写模板时未知的某种类型,但稍后将提供。
一旦定义了模板,编译器就可以使用该模板,根据需要生成任意多个重载函数(或类),每个函数使用不同的实际类型!
最终的结果是相同的——我们最终得到了一组基本相同的函数或类(每个函数或类对应一组不同的类型)。但我们只需要创建和维护一个模板,编译器为我们做所有的重复和艰苦的工作。
由于直到模板在程序中使用(而不是在编写模板时)才确定实际类型,因此模板的作者不必尝试预测可能使用的所有实际类型。这意味着模板代码可以与编写模板时甚至不存在的类型一起使用!稍后,当我们开始探索C++标准库时,我们将看到这是如何派上用场的,该库绝对充满了模板代码!
在本节的其余部分中,我们将介绍和探索如何为函数创建模板,并更详细地描述它们的工作方式。直到我们介绍了类是什么,我们才会探索如何为类创建模版。
关键点
编译器可以使用单个模板来生成一系列相关的函数或类,每个函数或类使用一组不同的类型。
旁白
因为模板背后的概念很难用语言描述,所以让我们尝试一个类比。
如果您要在字典中查找单词“template”,您会发现一个类似于以下的定义:“模板是用作创建类似对象的模式的模型”。一种非常容易理解的模板类型是模板。模板是一块薄薄的材料(例如一块纸板或塑料),用它切割出一个形状(例如一张快乐的脸)。通过将模具放在另一个对象的顶部,然后通过孔喷涂油漆,可以非常快速地复制剪切形状。模具本身只需要创建一次,然后可以根据需要重复使用多次,以创建任意多个不同颜色的剪切形状。更好的是,在实际使用模具之前,不必确定使用的颜色。
模板本质上是用于创建函数或类的模具。我们创建一次模板(模具),然后可以根据需要多次使用它,为一组特定的实际类型模板化函数或类。在实际使用模板之前,不需要确定这些实际类型。
关键点
模板可以与编写模板时甚至不存在的类型一起工作。这有助于使模板代码既灵活又经得起未来考验!
函数模板
函数模板是一种类似于函数的定义,用于生成一个或多个重载函数,每个函数具有一组不同的实际类型。这将允许我们创建可以与许多不同类型一起工作的函数。
创建函数模板时,我们将类型占位符(也称为模板类型参数,type template parameters)用于希望稍后指定的函数中使用的任何参数类型、返回类型或函数体中变量的类型。
函数模板,最好通过示例来教授,因此让我们将上面示例中的int max(int, int)函数转换为函数模板。这出人意料的简单,我们将解释一路上发生的事情。
对于高级读者
C++支持3种不同的模板参数:
- 模板类型参数(其中模板参数表示类型)。
- 模板非类型参数(其中模板参数表示constexpr值)。
- 模板模板参数(其中模板参数表示模板)。
到目前为止,模板类型参数是最常见的,因此我们将重点讨论。我们将在关于数组的一章中介绍非模板类型参数。
创建模板化的max函数
这里是max的int版本:
|
|
注意,我们在这个函数中三次使用int类型:一次用于参数x,一次用于参数y,一次用作函数的返回类型。
要创建函数模板,我们要做两件事。首先,我们将用模板类型参数替换我们的特定类型。在这种情况下,因为我们只有一个需要替换的类型(int),所以我们只需要一个模板类型参数(这里将其称为T):
下面是使用单个模板类型的新函数:
|
|
这是一个好的开始——然而,它无法编译,因为编译器不知道T是什么!这仍然是一个普通函数,不是函数模板。
因此,我们要告诉编译器,这是一个函数模板,T是一个模板类型参数,它是任何类型的占位符。这是使用所谓的模板参数声明来完成的。模板参数声明的范围仅限于后面的函数模板(或类模板)。因此,每个函数模板(或类模版)都需要自己的模板参数声明。
|
|
在模板参数声明中,我们从关键字template开始,它告诉编译器我们正在创建模板。接下来,我们指定所有模板参数,这些都在尖括号(<>)内。对于每个模板类型参数,我们使用关键字typename或class,后跟模板类型参数的名称(例如T)。
旁白
在此上下文中,typename和class关键字之间没有区别。您将经常看到人们使用class关键字,因为它是早期引入C++语言的。然而,我们更喜欢较新的typename关键字,因为它使模板类型参数可以被任何类型(例如基本类型)取代,而不仅仅是类类型。
信不信由你,我们完工了!我们已经创建了max函数的模板版本,现在可以接受不同类型的参数。
因为该函数模板有一个名为T的模板类型,所以我们将其称为max<T>。在下一课中,我们将看看如何使用max<T>函数模板来生成一个或多个具有不同类型参数的max()函数。
命名模板参数
就像我们经常使用单个字母来表示在琐碎情况下使用的变量名(例如x)一样,当类型的含义琐碎或明显时,通常使用单个大写字母(以T开头)。例如,在max函数模板中:
|
|
我们不需要给T一个复杂的名称,因为它显然只是要比较的值的占位符类型。
我们的函数模板通常使用这种命名约定。
如果模板类型参数的用法或含义不明显,则需要更具描述性的名称。此类名称有两种常见的约定:
- 以大写字母开头(例如,Allocator)。标准库使用此命名约定。
- 前缀为T,然后以大写字母开头(例如TAllocator)。这使得更容易看到类型是模板类型参数。
你选择哪一个是个人偏好的问题。
对于高级读者
例如,标准库有一个std::max重载,声明如下:
|
|
因为a和b是T类型的,我们知道不用关心a和b的类型——它们可以是任何类型。因为comp具有类型Compare,所以我们知道comp必须是满足Compare要求的类型。我们可以参考技术文档来确定这些特定要求是什么。
最佳实践
可以使用以T开始的单个大写字母(例如T、U、V等…)来命名以通常的方式使用的模板类型参数。
如果模板类型参数的用法或含义不明显,则需要更具描述性的名称。(例如,Allocator或TAllocator)。
