Lesson34 模板

在之前,如果想实现同名函数的不同版本共存,我们通常使用重载函数来实现。然而,基本参数类型有很多,再加上用户自定义类型,要维护这些重载函数绝非易事。(Note:还记得 “酒吧的炒饭” 吗?)

直到现在,我们依然缺乏一种通用函数方法,使之可以兼容任何传入的参数类型。而 C++ 的模板 (template) 就是为了解决这类通用问题而生的。

在 C++ 中,模板系统是用来简化创建能够处理不同数据类型的函数(或类)的过程。我们不需要创建一大堆非常相似的函数或类,只需要创建一个单独的模板。

和正常定义一样,模板定义了函数和类的样子。但与前者不同的是,模板使用了占位符类型 (placeholder type)。占位符类型在模板定义时未知,在使用时确定实际类型后才填入。

一旦定义了模板,编译器就可以使用该模板生成所需的重载函数(或类)。换句话说,我们将创建重载函数的工作交给了编译器,让它根据我们设置的规则(模板)结合实际类型来创建对应的重载函数(或类)。

由于实际类型在模板使用时才确定,因此模板代码可以和定义时尚未存在的类型一起使用。这赋予了程序灵活性,有助于应对未来的需求变化。

34.1 函数模板

函数模板 (function template) 是一种类似于一般函数的定义,用于生成具有不同函数类型的重载函数。用于生成其他函数的初始函数模板称为主要模板 (primary template),从主要模板生成的函数称为实例化函数 (instantiated function)。

当我们创建一个主要函数模板时,我们先定义一个占位符类型(也称为类型模板参数 (type template parameter),或模板类型 (template type),或泛型类型 (generic type))来表示任何参数类型返回类型函数体中使用的类型,这些类型将在稍后由模板的使用者指定。

要创建函数模板很简单:

1
2
3
4
5
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 )。

当模板参数以简单或明显的方式使用时,通常使用单个大写字母(从 T 开始)。

我们不需要给 T 赋予一个复杂的名称,如果类型模板参数有非显而易见的用法或必须满足的特定要求,此类名称通常有两种常见的约定:

  • 以大写字母开头(例如 Allocator )标准库使用这种命名约定。
  • 前缀为 T ,然后以大写字母开头(例如 TAllocator )。这使得更容易看出类型是一个模板参数类型。

可以创建同时具有模板参数和非模板参数的函数模板。类型模板参数可以与任何类型匹配,而非模板参数则像常规函数的参数一样工作。

1
2
3
4
5
template <typename T>
int someFcn(T, double)
{
return 5;
}

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
2
3
4
int max<int>(int x, int y) // the generated function max<int>(int, int)
{
return (x < y) ? y : x;
}

在大多数情况下,我们想要实例化的实际类型将与我们的函数参数类型相匹配。在这种情况下,我们不需要指定实际类型 —— 相反,我们可以使用模板参数推导 (template argument deduction),让编译器从函数调用中的参数类型推导出应该使用的实际类型。

1
2
std::cout << max<>(1, 2) << '\n';
std::cout << max(1, 2) << '\n';

编译器会看到我们没有提供实际类型,因此它将尝试从函数参数中推断出来,以便生成一个所有模板参数都与提供的参数类型匹配的 max() 函数。在这个例子中,编译器将推断出使用函数模板 max<T> 和实际类型 int 可以使其实例化函数 max<int>(int, int),这样函数参数的类型就与提供的参数的类型相匹配。

在上一种情况(使用空尖括号)中,编译器在确定要调用哪个重载函数时,只会考虑 max<int> 模板函数重载。在下一种情况(没有尖括号)中,编译器将考虑 max<int> 模板函数重载和 max 非模板函数重载。当下一种情况导致模板函数和非模板函数都同样可行时,将优先选择非模板函数

为什么优先调用非模板函数

一个非模板函数只处理特定类型的组合。它可能有一个比函数模板版本更优化或更针对那些特定类型的实现。

在大多数情况下,我们将使用正常的函数调用语法(即 max(1, 2))来调用从函数模板实例化的函数。


实例化函数不总是能通过编译。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
T addOne(T x)
{
return x + 1;
}

int main()
{
std::string hello { "Hello, world!" };
std::cout << addOne(hello) << '\n';

return 0;
}

hello + 1 没有意义,于是编译错误。


编译器只要在语法上合理,就能成功编译实例化的函数模板。然而,编译器并没有任何方法来检查这样的函数在语义上是否合理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

template <typename T>
T addOne(T x)
{
return x + 1;
}

int main()
{
std::cout << addOne("Hello, world!") << '\n';

return 0;
}

给一个字符串字面量加 1,有什么意义?但 C++ 在语法上允许将整数值添加到字符串字面量中,于是编译通过,输出:

1
ello, world!
Warning

确保你调用函数模板时使用有意义的参数,这是你的责任。


当在函数模板中使用静态局部变量时,从该模板实例化的每个函数都将有一个独立的静态局部变量版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <>
void printIDAndValue<int>(int value)
{
static int id{ 0 };
std::cout << ++id << ") " << value << '\n';
}

template <>
void printIDAndValue<double>(double value)
{
static int id{ 0 };
std::cout << ++id << ") " << value << '\n';
}

printIDAndValue<int>printIDAndValue<double> 各自都有一个名为 id 的独立静态局部变量,而不是它们之间共享的变量。printIDAndValue<int> 中的 ++id 不会影响 printIDAndValue<double> 中的 id 的值。

Tip

一个很好的经验法则是首先创建普通函数,然后如果你发现你需要为不同参数类型创建重载,再将其转换为函数模板。

34.1.2 具有多个模板类型的函数模板


©2025-Present Watermelonabc | 萌ICP备20251229号

Powered by Hexo & Stellar latest & Vercel & 𝙌𝙞𝙪𝙙𝙪𝙣 𝘾𝘿𝙉 & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 41 篇文章 · 总计 209.8k 字