C++ 模板特化

2025年03月17日 | 阅读 9 分钟

为什么要使用模板?

C++ 要求我们为变量、函数和其他实体使用特定的类型来声明。然而,对于不同的类型,很多代码看起来是相同的。特别是如果我们实现算法,比如快速排序,或者数据结构,比如链表或二叉树,对于不同的类型,代码看起来是相同的,尽管使用的类型不同。假设我们的编程语言不支持这种特殊的语言特性。在这种情况下,我们只有糟糕的选择:

  1. 我们可以为每种需要这种行为的类型重复实现相同的行为。
  2. 我们可以为通用基类型(如 Object 或 void*)编写通用代码。
  3. 我们可以使用特殊的预处理器。

如果我们来自 C、Java 或类似语言,我们以前做过一些或全部这些事情。然而,每种方法都有其缺点:

  1. 如果我们一遍又一遍地实现一个行为,我们就是在重复造轮子。我们会犯同样的错误,并因为避免复杂但更好的算法会导致更多错误而放弃它们。
  2. 如果我们为通用基类编写通用代码,我们会失去类型检查的好处。此外,类可能需要从特殊的基类派生,这使得维护代码更加困难。
  3. 我们通过使用像 C/C++ 预处理器这样的特殊预处理器失去了格式化源代码的优势。代码被一个“愚蠢的文本替换机制”替换,而没有任何作用域和类型概念。

模板是解决这个问题的方案,没有这些缺点。它们是为一种或多种尚未指定的类型编写的函数或类。当我们使用模板时,我们显式或隐式地将类型作为参数传递。由于模板是语言特性,我们拥有完整的类型检查和作用域支持。在今天的程序中,模板使用非常广泛。例如,在 C++ 标准库中,几乎所有代码都是模板代码。该库提供了用于对指定类型的对象和值进行排序的算法,用于管理选定类型元素的(所谓的容器)数据结构,用于参数化字符样式的字符串等等。然而,这仅仅是个开始。模板还允许我们参数化行为、优化代码和参数化信息。这将在后面的章节中介绍。让我们先从一些简单的模板开始。

类模板的特化

我们可以为特定的模板参数特化类模板。就像函数模板的重载一样,特化类模板允许我们为特定类型优化实现,或修复特定类型实例化类模板时的错误行为。然而,如果我们特化一个类模板,我们也必须特化所有成员函数。虽然可以特化单个成员函数,但一旦这样做,我们就不能再特化整个类了。

要特化类模板,我们必须使用 `template<>` 前缀声明类,并指定该类模板特化的类型。这些类型用作模板参数,并且必须直接跟在类名后面。

对于这些特化,任何成员函数的定义都必须作为“普通”成员函数来定义,其中 T 的每个出现都被替换为特化类型。

部分特化

类模板可以部分特化。我们可以为特定情况指定特化的实现,但用户仍需定义一些模板参数。例如,对于以下类模板:

以下示例显示了哪个模板被哪个声明使用:

如果多个部分特化匹配的程度相同,则声明是模棱两可的。

函数模板

函数模板是特殊的函数,可以处理通用类型。它允许我们创建一个函数模板,其功能可以适应多种类型或类,而无需为每种类型重复整个代码。

在 C++ 中,这可以通过模板参数来实现。模板参数是一种特殊的参数,可以用于将类型作为参数传递:就像常规函数参数可以用于将值传递给函数一样,模板参数也允许将类型传递给函数。这些函数模板可以像使用其他常规类型一样使用这些参数。

使用类型参数声明函数模板的格式是:

这两种原型之间的唯一区别是使用了 `class` 关键字或 `typename` 关键字。它们的使用是可区分的,因为这两种表达式具有相同的含义并且行为相似。

例如,要创建一个返回两个对象中较大者值的模板函数,我们可以使用:

在这里,我们创建了一个以 `myType` 作为其模板参数的模板函数。此模板参数代表一个尚未指定的类型,但可以在模板函数中使用,就像它是常规类型一样。如您所见,函数模板 `GetMax` 返回两个此尚未定义类型的参数中的较大者。要使用此函数模板,我们使用以下格式的函数调用:

function_name <type> (parameters);

例如,要调用 `GetMax` 来比较两个 `int` 类型的整数值,我们可以编写:

当编译器遇到对模板函数的此调用时,它使用模板自动生成一个函数,将 `myType` 的每个实例替换为作为实际模板参数传递的类型(在此情况下为 `int`),然后调用它。编译器会自动执行此过程,并且对程序员是不可见的。

这是整个示例:

在这种情况下,我们使用 `T` 作为模板参数名而不是 `myType`,因为它更短,并且是一个非常常见的模板参数名。但您可以使用任何您喜欢的标识符。

我们在上面的示例中两次使用了函数模板 `GetMax()`。第一次是使用 `int` 类型的参数,第二次是使用 `long` 类型的参数。编译器每次都实例化并调用了适当版本的函数。

可以看到,在 `GetMax()` 模板函数中,类型 `T` 甚至被用来声明该类型的新对象:

T result

因此,当使用特定类型实例化函数模板时,结果将是一个与参数 `a` 和 `b` 类型相同的对象。

在这种通用类型 `T` 用作 `GetMax` 的参数的特定情况下,编译器可以自动找出需要实例化哪种数据类型,而无需在尖括号中显式指定(就像我们之前指定 `<int>` 和 `<long>` 一样)。所以我们可以写成:

由于 `i` 和 `j` 都是 `int` 类型,编译器可以自动确定模板参数只能是 `int`。这种隐式方法会产生相同的结果。

请注意,在这种情况下,我们调用了函数模板 `GetMax()`,而没有在尖括号 `<>` 中显式指定类型。编译器会自动确定每次调用需要什么类型。

因为我们的模板函数只包含一个模板参数(`class T`),并且函数模板本身接受两个参数,这两个参数都是 `T` 类型,所以我们不能用两个不同类型的对象作为参数来调用我们的函数模板。

这是不正确的,因为我们的 `GetMax` 函数模板期望两个相同类型的参数,而在这次调用中,我们使用了两种不同类型的对象。

我们还可以通过在尖括号之间指定更多模板参数来定义接受多个参数类型的函数模板。例如:

在这种情况下,我们的函数模板 `GetMin()` 接受两个不同类型的参数,并返回与传递的第一个参数(`T`)类型相同的对象。例如,在此声明之后,我们可以使用以下方式调用 `GetMin()`:

即使 `j` 和 `l` 类型不同,编译器仍然可以确定适当的实例化。

类模板

我们还可以编写类模板,以便类拥有可以使用模板参数作为类型的成员。例如:

我们刚刚定义的类用于存储任何有效类型的两个元素。例如,如果我们想声明一个类对象来存储两个 `int` 类型的整数值,值为 115 和 36,我们将编写:

mypair myobject (115, 36);

同一个类也将用于创建对象来存储任何其他类型:

mypair myfloats (3.0, 2.18);

上一个类模板中唯一的成员函数已在类声明本身中内联定义。如果我们为类模板声明之外的成员函数定义一个函数,我们必须始终在定义前加上 `template <...>` 前缀。

输出

C++ Template Specialization

注意成员函数 `getmax` 定义的语法:

此声明包含三个 `T`:第一个是模板参数。第二个 `T` 指示函数返回的类型。第三个 `T`(尖括号之间的那个)也是一个要求:它指定此函数的模板参数也是类模板参数。

模板特化

如果我们想在将特定类型作为模板参数传递时为模板定义不同的实现,我们可以声明该模板的特化。例如,我们有一个非常简单的类 `mycontainer`,它可以存储任何类型的单个元素,并且只有一个名为 `increase` 的成员函数,该函数增加其值。但是我们发现,当它存储 `char` 类型元素时,拥有一个具有 `uppercase` 成员函数的完全不同的实现会更方便,因此我们决定为该类型声明一个类模板特化。

输出

C++ Template Specialization

下面是类模板特化中使用的语法:

首先,请注意我们在类模板名称前加上了一个空的 `template <>` 参数列表。这是为了明确声明它是一个模板特化。

但类模板名称后面的特化参数比这个前缀更重要。这个特化参数标识我们将为其声明模板类特化的类型(`char`)。请注意通用类模板和特化之间的区别:

第一行是通用模板,第二行是特化。当我们为模板类声明特化时,我们还必须定义其所有成员,即使是那些与通用模板类完全相同的成员,因为通用模板到特化的成员之间没有“继承”。