11-6 函数模板
假设你想编写一个计算两个数最大值的函数。可能这样实现:
int max(int x, int y)
{
return (x < y) ? y : x;
// Note: we use < instead of > because std::max uses <
}
虽然调用方可传入不同值,但形参类型是固定的,因此只能传入整型值。这意味着该函数仅适用于整数(及可提升为整型的类型)。
那么当需要计算两个双精度值的最大值时该怎么办?由于C++要求显式指定所有函数形参类型,解决方案是创建一个双精度形参的重载版本:
double max(double x, double y)
{
return (x < y) ? y: x;
}
注意双精度版本的实现代码与整数版本完全相同!事实上,这种实现适用于多种类型:包括 int、double、long、long double,甚至你自己创建的新类型(具体方法将在后续课程中讲解)。
为每组支持的形参类型创建具有相同实现的重载函数,不仅是维护噩梦,更是错误温床,更明显违反了DRY(不要重复自己)原则。这里还存在一个不易察觉的挑战:希望使用 max 函数的程序员,可能需要传入作者未预见的实参类型(因此未编写重载函数)。
我们真正缺失的是某种机制,能编写单一版本的max函数处理任意类型实参(甚至包括编写max代码时未预见的类型)。普通函数在此场景下显然力不从心。所幸C++支持另一项专为解决此类问题设计的特性。
欢迎进入C++模板的世界。
C++模板介绍
C++模板系统旨在简化创建多类型适配函数(或类)的过程。
无需手动创建大量近乎相同的函数或类(每种类型组合对应一个),我们只需定义单一模板。与普通定义类似,模板template定义描述了函数或类的结构框架。不同于普通定义(需明确指定所有类型),模板中可使用一个或多个占位符类型。占位符类型代表模板定义时未知、但后续使用时将被填入的具体类型。
模板定义完成后,编译器便能根据需求生成任意数量的重载函数(或类),每个实例都使用不同的实际类型!
最终结果相同——我们得到了一堆基本相同的函数或类(每种类型组合对应一个)。但我们只需创建和维护单个模板,编译器会自动完成其余部分的生成工作。
核心要义
编译器能利用单个模板生成相关函数或类的家族,每个成员使用不同的实际类型组合。
顺带一提……
由于模板背后的概念难以用语言描述,不妨尝试一个类比。若在词典中查阅“模板”一词,你会发现类似这样的定义:“模板是用于创建相似对象的模型范式”。最易理解的模板类型当属描绘板。这种薄片状材料(如硬纸板或塑料)上切割出特定形状(例如笑脸)。将描绘板置于物体表面,通过孔洞喷涂颜料,即可快速复制出切割形状。模板本身只需制作一次,即可无限次重复使用,用任意颜色塑造各种形状。更妙的是,模板制作的形状颜色无需预先确定,直到实际使用时才需决定。
模板本质上是用于创建函数或类的模板。我们只需创建一次模板(即我们的模板),便能根据具体实际类型需求反复使用它来生成函数或类。这些实际类型在模板实际使用前无需确定。
关键洞见
模板能处理在模板编写时尚未存在的类型。这使得模板代码既灵活又具备前瞻性!
本节后续内容将介绍函数模板的创建方法,并详细阐述其工作原理。类模板的讨论将留待讲解完类概念后进行。
函数模板
函数模板function template是一种类似函数的定义,用于生成一个或多个重载函数,每个函数都具有不同的实际类型组合。这正是我们能够创建适用于多种不同类型的函数的基础。用于生成其他函数的初始函数模板称为主模板primary template,而从主模板生成的函数则称为实例化函数instantiated functions。
当我们创建一个主函数模板时,会使用占位符类型placeholder types(技术上称为类型模板形参type template parameters,非正式地称为模板类型template types)来表示任何形参类型、返回类型或函数体中使用的类型,这些类型需要由模板使用者在后续指定。
对于进阶读者
C++支持三种不同的模板形参:
- 类型模板形参(模板形参代表类型)。
- 非类型模板形参(模板形参表示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 parameter declaration实现,该声明定义后续将使用的所有模板形参。模板形参声明的作用域严格限定于其后跟随的函数模板(或类模板)。因此每个函数模板或类模板都需要独立的模板形参声明。
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,我们知道它们的具体类型无关紧要——它们可以是任意类型。由于 comp 的类型为 Compare,我们知道 comp 必须是满足 Compare 要求(无论该要求是什么)的类型。
当函数模板被实例化时,编译器会用模板实参替换模板形参,然后编译生成的实例化函数。函数能否编译成功取决于函数内部如何使用每种类型的对象。因此,给定模板形参的要求本质上是隐式定义的。
由于难以从类型对象的使用方式推断要求,此时查阅技术文档便显得尤为重要——文档应明确说明相关要求。例如若需了解Compare的具体要求,可查阅std::max的文档(参见https://en.cppreference.com/w/cpp/algorithm/max),相关要求应在其中列明。
最佳实践
使用以大写字母T开头的单个字母(如T、U、V等)为类型模板形参命名,这些形参用于简单或显而易见的情境,代表“任何合理类型”。
若类型模板形参具有非显而易见的使用场景或需满足特定要求,则应采用更具描述性的名称(如Allocator或TAllocator)。
测验时间
问题 #1
描述建筑蓝图为何属于模板的一种类型。
显示答案
建筑蓝图本身只是一张描述如何建造建筑物的图纸。施工人员借助蓝图,能够快速建造出许多结构相同的建筑。然而,每栋建筑使用的具体材料类型可能有所不同(例如建筑外墙板采用何种材料),且这些细节无需在实际建造前确定。

浙公网安备 33010602011771号