一、编译器如何处理模板
1.模板代码的处理
为了理解模板的复杂性,你需要了解编译器是如何处理模板代码的。当编译器遇到模板方法定义时,它会进行语法检查,但实际上不会编译模板。编译器不能编译模板定义,因为它不知道这些模板将用于哪些类型。编译器不可能为像 x = y 这样的代码生成代码,而不知道 x 和 y 的类型。
当编译器遇到模板的实例化,例如 Grid,它会通过将类模板定义中的每个 T 替换为 int 来为 int 版本的 Grid 模板编写代码。当编译器遇到模板的不同实例化,例如 Grid,它会为 SpreadsheetCell 编写另一个版本的 Grid 类。编译器只是写出了如果没有模板支持,你需要为每种元素类型编写单独类时的代码。这里没有魔法;模板只是自动化了一个烦人的过程。如果你在程序中没有为任何类型实例化类模板,那么类方法定义就永远不会被编译。
这种实例化过程解释了为什么你需要在定义的各个地方使用 Grid 语法。当编译器为特定类型(如 int)实例化模板时,它会将 T 替换为 int,使 Grid 成为该类型。
2.选择性实例化
对于隐式类模板实例化,如以下示例:
Grid myIntGrid;
编译器总是为类模板的所有虚拟方法生成代码。然而,对于非虚拟方法,编译器只为你实际调用的那些非虚拟方法生成代码。例如,给定前面的 Grid 类模板,假设你在 main() 中写了这样的代码(仅此代码):
Grid myIntGrid;
myIntGrid.at(0, 0) = 10;
编译器仅为 int 版本的 Grid 生成无参数构造函数、析构函数和非 const 的 at() 方法。它不会生成其他方法,如拷贝构造函数、赋值运算符或 getHeight()。这被称为选择性实例化。
存在的风险是,某些类模板方法中的编译错误可能会被忽略。未使用的类模板方法可能包含语法错误,因为这些不会被编译。这使得测试所有代码的语法错误变得困难。
你可以通过使用显式模板实例化来强制编译器为所有方法(虚拟和非虚拟)生成代码。以下是一个示例:
template class Grid;
注意:显式模板实例化有助于发现错误,因为它强制编译器编译所有即使未使用的类模板方法。使用显式模板实例化时,不要只尝试使用基本类型(如 int)实例化类模板,还要尝试使用更复杂的类型(如 string)。
二、模板对类型的要求
1.类型独立的代码编写
当你编写与类型无关的代码时,必须对这些类型做出某些假设。例如,在 Grid 类模板中,你假设元素类型(由 T 表示)是可销毁的、可拷贝/移动构造的,以及可拷贝/移动赋值的。
当编译器尝试用不支持类模板方法所使用的所有操作的类型来实例化模板时,代码将无法编译,且错误消息通常相当晦涩难懂。
然而,即使你想使用的类型不支持类模板的所有方法所需的操作,你也可以利用选择性实例化来使用某些方法而不是其他方法。
2.C++20 引入的概念(Concepts)
C++20 引入了概念(concepts),允许你为模板参数编写编译器可以解释和验证的要求。如果传递给模板实例化的模板参数不满足这些要求,编译器可以生成更易读的错误消息。后面将讨论概念。
概念为模板编程增加了额外的类型安全性,它通过为模板参数提供一个明确的接口合约来实现。这种方式不仅可以防止类型不匹配的问题,还可以改善模板错误消息的可读性,从而使模板代码更容易维护和理解。
三、类模板代码的文件
在类模板中,类模板定义和方法定义必须对任何使用它们的源文件可用。有几种机制可以实现这一点:
1.方法定义与类模板定义在同一文件
你可以将方法定义直接放在定义类模板本身的模块接口文件中。当你在另一个源文件中导入这个模块以使用模板时,编译器将能够访问它所需的所有代码。这种机制用于之前的 Grid 实现。
2.方法定义在单独的文件
或者,你可以将类模板方法定义放在一个单独的模块接口分区文件中。然后,你还需要将类模板定义放在自己的分区中。例如,Grid 类模板的主模块接口文件可能如下所示:
export module grid;
export import :definition;
export import :implementation;
这导入并导出了两个模块分区:定义(definition)和实现(implementation)。类模板定义在定义分区中定义:
export module grid:definition;
import ;
import ;
export template class Grid { ... };
方法的实现位于实现分区中,该分区还需要导入定义分区,因为它需要 Grid 类模板定义:
export module grid:implementation;
import :definition;
import ;
...
export template Grid::Grid(size_t width, size_t height)
: m_width { width }, m_height { height } { ... }