Lesson34 模板
在之前,如果想实现同名函数的不同版本共存,我们通常使用重载函数来实现。然而,基本参数类型有很多,再加上用户自定义类型,要维护这些重载函数绝非易事。(Note:还记得 “酒吧的炒饭” 吗?)
直到现在,我们依然缺乏一种通用函数方法,使之可以兼容任何传入的参数类型。而 C++ 的模板 (template) 就是为了解决这类通用问题而生的。
在 C++ 中,模板系统是用来简化创建能够处理不同数据类型的函数(或类)的过程。我们不需要创建一大堆非常相似的函数或类,只需要创建一个单独的模板。
和正常定义一样,模板定义了函数和类的样子。但与前者不同的是,模板使用了占位符类型 (placeholder type)。占位符类型在模板定义时未知,在使用时确定实际类型后才填入。
一旦定义了模板,编译器就可以使用该模板生成所需的重载函数(或类)。换句话说,我们将创建重载函数的工作交给了编译器,让它根据我们设置的规则(模板)结合实际类型来创建对应的重载函数(或类)。
由于实际类型在模板使用时才确定,因此模板代码可以和定义时尚未存在的类型一起使用。这赋予了程序灵活性,有助于应对未来的需求变化。
34.1 函数模板
函数模板 (function template) 是一种类似于一般函数的定义,用于生成具有不同函数类型的重载函数。用于生成其他函数的初始函数模板称为主要模板 (primary template),从主要模板生成的函数称为实例化函数 (instantiated function)。
当我们创建一个主要函数模板时,我们先定义一个占位符类型(也称为类型模板参数 (type template parameter),或模板类型 (template type),或泛型类型 (generic type))来表示任何参数类型、返回类型或函数体中使用的类型,这些类型将在稍后由模板的使用者指定。
要创建函数模板很简单:
1 | template <typename T> // this is the template parameter declaration defining T as a type template parameter |
在我们的模板参数声明中,我们首先使用关键字 template
,这告诉编译器我们正在创建一个模板。接下来,我们指定模板将使用的所有模板参数,这些参数位于尖括号 <>
内。对于每个类型模板参数,我们使用关键字 typename
(推荐)或 class
,后跟类型模板参数的名称(例如 T
)。
当模板参数以简单或明显的方式使用时,通常使用单个大写字母(从 T
开始)。
我们不需要给 T
赋予一个复杂的名称,如果类型模板参数有非显而易见的用法或必须满足的特定要求,此类名称通常有两种常见的约定:
- 以大写字母开头(例如
Allocator
)标准库使用这种命名约定。 - 前缀为
T
,然后以大写字母开头(例如TAllocator
)。这使得更容易看出类型是一个模板参数类型。
可以创建同时具有模板参数和非模板参数的函数模板。类型模板参数可以与任何类型匹配,而非模板参数则像常规函数的参数一样工作。
1 | template <typename T> |
34.1.1 使用函数模板
要使用我们的 `max
1 | max<actual_type>(arg1, arg2); // actual_type is some actual type, like int or double |
这看起来很像一个普通的函数调用 —— 主要区别是添加了尖括号中的类型,它指定了将用于替换模板类型 T
的实际类型。
1 | std::cout << max<int>(1, 2) << '\n'; // instantiates and calls function max<int>(int, int) |
当编译器遇到函数调用 max<int>(1, 2)
时,它发现 max<int>(int, int)
的函数定义尚未存在。因此,编译器将隐式地使用 max<T>
函数模板来创建一个。
从函数模板(具有模板类型)创建函数(具有特定类型)的过程称为函数模板实例化 (function template instantiation)。当一个函数由于函数调用而实例化时,它被称为隐式实例化 (implicit instantiation)。从模板实例化的函数通常称为函数实例 (function instance)。函数实例在所有方面都是普通函数。
实例化函数的过程很简单:编译器基本上是克隆了主模板,并将模板类型 T
替换为我们指定的实际类型 int
。
因此,当我们调用 max<int>(1, 2)
时,实例化的函数看起来就像这样:
1 | int max<int>(int x, int y) // the generated function max<int>(int, int) |
在大多数情况下,我们想要实例化的实际类型将与我们的函数参数类型相匹配。在这种情况下,我们不需要指定实际类型 —— 相反,我们可以使用模板参数推导 (template argument deduction),让编译器从函数调用中的参数类型推导出应该使用的实际类型。
1 | std::cout << max<>(1, 2) << '\n'; |
编译器会看到我们没有提供实际类型,因此它将尝试从函数参数中推断出来,以便生成一个所有模板参数都与提供的参数类型匹配的 max()
函数。在这个例子中,编译器将推断出使用函数模板 max<T>
和实际类型 int
可以使其实例化函数 max<int>(int, int)
,这样函数参数的类型就与提供的参数的类型相匹配。
在上一种情况(使用空尖括号)中,编译器在确定要调用哪个重载函数时,只会考虑 max<int>
模板函数重载。在下一种情况(没有尖括号)中,编译器将考虑 max<int>
模板函数重载和 max
非模板函数重载。当下一种情况导致模板函数和非模板函数都同样可行时,将优先选择非模板函数。
一个非模板函数只处理特定类型的组合。它可能有一个比函数模板版本更优化或更针对那些特定类型的实现。
在大多数情况下,我们将使用正常的函数调用语法(即 max(1, 2)
)来调用从函数模板实例化的函数。
实例化函数不总是能通过编译。例如:
1 | template <typename T> |
hello + 1
没有意义,于是编译错误。
编译器只要在语法上合理,就能成功编译实例化的函数模板。然而,编译器并没有任何方法来检查这样的函数在语义上是否合理。
1 |
|
给一个字符串字面量加 1,有什么意义?但 C++ 在语法上允许将整数值添加到字符串字面量中,于是编译通过,输出:
1 | ello, world! |
确保你调用函数模板时使用有意义的参数,这是你的责任。
当在函数模板中使用静态局部变量时,从该模板实例化的每个函数都将有一个独立的静态局部变量版本。
1 | template <> |
printIDAndValue<int>
和 printIDAndValue<double>
各自都有一个名为 id
的独立静态局部变量,而不是它们之间共享的变量。printIDAndValue<int>
中的 ++id
不会影响 printIDAndValue<double>
中的 id
的值。
一个很好的经验法则是首先创建普通函数,然后如果你发现你需要为不同参数类型创建重载,再将其转换为函数模板。