假设你想编写一个函数来计算两个数字的最大值。你可能会这样做:
int max(int x, int y)
{
return (x < y) ? y : x;
// Note: we use < instead of > because std::max uses <
}
虽然调用者可以向函数传递不同的值,但参数的类型是固定的,因此调用者只能传入int
值。这意味着这个函数只适用于整数(以及可以提升为int
的类型)。
那么,当你以后想找到两个double
值的最大值时会发生什么呢?因为C++要求我们指定所有函数参数的类型,所以解决方案是创建一个新的重载版本的max
,其参数类型为double
。
double max(double x, double y)
{
return (x < y) ? y: x;
}
请注意,max
的double版本的实现代码与int版本的max
完全相同!事实上,这个实现适用于许多不同的类型:包括int
、double
、long
、long double
,甚至是你自己创建的新类型(我们将在未来的课程中介绍如何实现)。
不得不为每组我们想要支持的参数类型创建具有相同实现的重载函数,这是一个维护上的麻烦,是错误的根源,并且明显违反了DRY(不要重复自己)原则。这里还有一个不那么明显的挑战:希望使用max
函数的程序员可能希望使用max
的作者没有预料到的参数类型(因此没有为其编写重载函数)来调用它。
我们真正缺少的是一种方法来编写一个单一版本的max
,它可以使用任何类型的参数(即使是编写max
代码时可能没有预料到的类型)。普通的函数根本无法胜任这项任务。幸运的是,C++支持另一种专门为解决这类问题而设计的功能。
欢迎来到C++模板的世界。
C++模板简介
在C++中,模板系统旨在简化创建能够使用不同数据类型的函数(或类)的过程。
我们不是手动创建一堆大体相同的函数或类(每组不同类型一个),而是创建一个单一的模板。就像普通的定义一样,模板定义描述了函数或类的样子。与普通定义(所有类型都必须指定)不同,在模板中我们可以使用一个或多个占位符类型。占位符类型表示在定义模板时未知的某些类型,但将在以后(使用模板时)提供。
一旦定义了模板,编译器就可以使用该模板根据需要生成尽可能多的重载函数(或类),每个都使用不同的实际类型!
最终结果是相同的——我们最终得到了一堆大体相同的函数或类(每组不同类型一个)。但是我们只需要创建和维护一个模板,编译器会为我们完成所有繁重的工作来创建其余部分。
关键见解
编译器可以使用单个模板生成一系列相关函数或类,每个都使用一组不同的实际类型。
题外话…
因为模板背后的概念很难用语言描述,所以我们尝试一个类比。
如果你在字典中查找“模板”这个词,你会发现一个类似于以下的定义:“模板是一种模型,它作为创建类似对象的模式。”一种非常容易理解的模板是模具。模具是一块薄材料(例如一块纸板或塑料),上面切出一个形状(例如一个笑脸)。通过将模具放在另一个物体上,然后通过孔喷漆,你可以非常快速地复制切出的形状。模具本身只需要创建一次,然后可以根据需要重复使用多次,以你喜欢的不同颜色创建切出的形状。更好的是,用模具制作的形状的颜色不需要在实际使用模具之前确定。
模板本质上是创建函数或类的模具。我们创建模板(我们的模具)一次,然后可以根据需要多次使用它,为一组特定的实际类型创建函数或类。这些实际类型不需要在实际使用模板之前确定。
因为实际类型直到程序中使用模板(而不是编写模板时)才确定,所以模板的作者不必尝试预测所有可能使用的实际类型。这意味着模板代码可以与编写模板时甚至不存在的类型一起使用!我们稍后在探索C++标准库时会发现这很有用,C++标准库完全由模板代码构成!
关键见解
模板可以与编写模板时甚至不存在的类型一起使用。这有助于使模板代码既灵活又面向未来!
在本课的其余部分,我们将介绍并探讨如何创建函数模板,并更详细地描述它们的工作原理。我们将把类模板的讨论推迟到我们涵盖了类是什么之后。
函数模板
函数模板是一种类似函数的定义,用于生成一个或多个重载函数,每个函数都带有一组不同的实际类型。这将使我们能够创建能够处理许多不同类型的函数。用于生成其他函数的初始函数模板称为主模板,从主模板生成的函数称为实例化函数。
当我们创建主函数模板时,我们使用占位符类型(技术上称为类型模板参数,非正式地称为模板类型)来表示任何参数类型、返回类型或函数体中使用的类型,这些类型我们希望稍后由模板的用户指定。
致进阶读者
C++支持3种不同类型的模板参数
- 类型模板参数(模板参数表示一个类型)。
- 非类型模板参数(模板参数表示一个 constexpr 值)。
- 模板模板参数(模板参数表示一个模板)。
类型模板参数是目前最常见的,所以我们首先关注它们。我们还将讨论非类型模板参数,它们在现代C++中使用量日益增加。
函数模板最好通过示例来教授,所以让我们将上面示例中的普通max(int, int)
函数转换为函数模板。这出奇地容易,我们将在此过程中解释发生了什么。
创建max()
函数模板
这是max()
的int
版本:
int max(int x, int y)
{
return (x < y) ? y : x;
}
请注意,在此函数中,我们三次使用了类型int
:一次用于参数x
,一次用于参数y
,一次用于函数的返回类型。
要为max()
创建一个函数模板,我们将做两件事。首先,我们将用类型模板参数替换任何我们希望稍后指定的实际类型。在这种情况下,因为我们只有一个需要替换的类型(int
),所以我们只需要一个类型模板参数(我们称之为T
)
这是我们使用单个模板类型的新函数,其中所有实际类型int
的出现都已替换为类型模板参数T
T max(T x, T y) // won't compile because we haven't defined T
{
return (x < y) ? y : x;
}
这是一个很好的开始——但是,它无法编译,因为编译器不知道T
是什么!而且这仍然是一个普通函数,而不是函数模板。
其次,我们将告诉编译器这是一个模板,并且T
是一个类型模板参数,它是任何类型的占位符。这两者都通过模板参数声明完成,它定义了随后将使用的任何模板参数。模板参数声明的作用域严格限制在它后面的函数模板(或类模板)。因此,每个函数模板或类模板都需要自己的模板参数声明。
template <typename T> // this is the template parameter declaration defining T as a type template parameter
T max(T x, T y) // this is the function template definition for max<T>
{
return (x < y) ? y : x;
}
在我们的模板参数声明中,我们首先使用关键字template
,它告诉编译器我们正在创建一个模板。接下来,我们用尖括号(<>
)指定模板将使用的所有模板参数。对于每个类型模板参数,我们使用关键字typename
(首选)或class
,后跟类型模板参数的名称(例如T
)。
相关内容
我们将在课程11.8 -- 具有多个模板类型的函数模板中讨论如何创建具有多个模板类型的函数模板。
题外话…
在此上下文中,typename
和class
关键字之间没有区别。你经常会看到人们使用class
关键字,因为它更早地引入到语言中。然而,我们更喜欢更新的typename
关键字,因为它更清楚地表明类型模板参数可以被任何类型(例如基本类型)替换,而不仅仅是类类型。
信不信由你,我们完成了!我们已经创建了max()
函数的模板版本,它可以接受不同类型的参数。
在下一课中,我们将探讨如何使用我们的max
函数模板来生成一个或多个具有不同类型参数的max()
函数,并实际调用这些函数。
模板参数命名
就像我们在简单情况下经常为变量名使用单个字母(例如x
)一样,当模板参数以简单或显而易见的方式使用时,通常使用单个大写字母(从T
开始)。例如,在我们的max
函数模板中:
template <typename T>
T max(T x, T y)
{
return (x < y) ? y : x;
}
我们不需要给T
一个复杂的名称,因为它显然只是用于比较值的占位符类型,并且T
可以是任何可比较的类型(例如int
、double
或char
,但不能是nullptr
)。
我们的函数模板通常会使用这种命名约定。
如果类型模板参数的用法不明显或必须满足特定要求,则此类名称有两种常见约定:
- 以大写字母开头(例如
Allocator
)。标准库使用这种命名约定。 - 以
T
作为前缀,然后以大写字母开头(例如TAllocator
)。这使得更容易看出该类型是类型模板参数。
选择哪种是个人偏好问题。
致进阶读者
例如,标准库有一个std::max()
的重载,其声明如下:
template< class T, class Compare >
const T& max( const T& a, const T& b, Compare comp ); // ignore the & for now, we'll cover these in a future lesson
因为a
和b
的类型是T
,所以我们知道我们不关心a
和b
的类型——它们可以是任何类型。因为comp
的类型是Compare
,所以我们知道comp
必须是满足Compare
要求的类型(无论那是什么)。
当函数模板实例化时,编译器将模板参数替换为模板实参,然后编译生成的实例化函数。函数是否编译取决于函数中每种类型的对象如何使用。因此,给定模板参数的要求基本上是隐式定义的。
因为很难从对象类型的使用方式推断出要求,所以这是一个需要查阅技术文档的领域,文档应该明确说明要求。例如,如果我们要知道Compare
的要求是什么,我们可以查阅std::max
的文档(例如参见https://cppreference.cn/w/cpp/algorithm/max),它应该列在那里。
最佳实践
对于以简单或显而易见的方式使用并表示“任何合理类型”的类型模板参数,使用单个大写字母(例如T
、U
、V
等)命名。
如果类型模板参数的用法不明显或必须满足特定要求,则需要更具描述性的名称(例如Allocator
或TAllocator
)。
小测验时间
问题 #1
描述为什么施工蓝图是一种模板。