C++泛型编程

类模板

编译器如何处理模板

当编译器遇到模板的实例化时,例如 Grid,它会通过将类模板定义中的每个 T 替换为 int 来为 Grid 模板的 int 版本编写代码。当编译器遇到模板的不同实例化时,例如 Grid,它会为 SpreadsheetCells 编写另一个版本的 Grid 类。如果语言中没有模板支持并且必须为每种元素类型编写单独的类,编译器只会编写你将要编写的代码。这里没有魔法;模板只会自动执行一个烦人的过程。如果不为程序中的任何类型实例化类模板,则永远不会编译类方法定义。

此实例化过程解释了为什么需要在定义的不同位置使用 Grid 语法。当编译器为特定类型(例如 int)实例化模板时,它会将 T 替换为 int,因此 Grid 就是该类型。

选择性实例化

对于隐式类模板实例化,如下所示:

Grid<int> myIntGrid;

编译器总是为类模板的所有虚方法生成代码。然而,对于非虚方法,编译器只为那些你实际调用的非虚方法生成代码。例如,给定前面的 Grid 类模板,假设你在 main() 中编写此代码(且仅此代码):

Grid<int> myIntGrid;
myIntGrid.at(0, 0) = 10;

编译器仅为 Grid 的 int 版本生成零参数构造函数、析构函数和非常量 at() 方法。它不会生成其他方法,如复制构造函数、赋值运算符或 getHeight()。这称为选择性实例化

危险在于某些类模板方法中存在未被注意的编译错误。未使用的类模板方法可能包含语法错误,因为这些方法不会被编译。这使得很难测试所有代码的语法错误。可以使用显式模板实例化强制编译器为所有方法(虚拟和非虚拟)生成代码。这是一个例子:

template class Grid<int>;

类型的模板要求

当你编写类型无关的代码时,必须假设这些类型的某些事情。例如,在 Grid 类模板中,你假设元素类型(由 T 表示)是可破坏的、复制/移动可构造的和复制/移动可赋值的。

当编译器尝试使用不支持类模板方法使用的所有操作的类型实例化模板时,代码将无法编译,并且错误消息通常会非常模糊。但是,即使你要使用的类型不支持类模板的所有方法所需的操作,你也可以利用选择性实例化来使用某些方法而不使用其他方法。

C++20 引入了 concepts,允许你编写编译器可以解释和验证的模板参数要求。如果传递给实例化模板的模板参数不满足这些要求,编译器会生成更易读的错误。

在文件之间分发模板代码

对于类模板,类模板定义和方法定义都必须对可供编译器从使用它们的任何源文件可用。有几种机制可以实现这一点。

  • 与类模板定义相同的文件中的方法定义:你可以将方法定义直接放在定义类模板本身的模块接口文件中。当你将此模块导入另一个使用模板的源文件时,编译器将可以访问它需要的所有代码。这种机制用于之前的 Grid 实现。
  • 单独文件中的方法定义:或者,你可以将类模板方法定义放在单独的模块接口分区文件中。然后你还需要将类模板定义放在它自己的分区中。例如,Grid 类模板的主要模块接口文件可能如下所示:
export module grid;
export import :definition;
export import :implementation;

这导入和导出两个模块分区:定义和实现。类模板定义在定义分区中:

export module grid:definition;
import ;
import ;
export template <typename T> class Grid { ... };

方法的实现在实现分区中,也需要引入定义分区,因为它需要Grid类模板定义:

export module grid:implementation;
import :definition;
import ;
...
export template <typename T> Grid<T>::Grid(size_t width, size_t height) : m_width { width }, m_height { height }
{ ... }

模板参数

非类型模板参数

非类型参数是“普通”参数,例如 int 和指针——你熟悉的函数和方法中的参数类型。但是,非类型模板参数只能是整数类型(char、int、long 等)、枚举类型、指针、引用、std::nullptr_t、auto、auto& 和 auto*。 C++20 还允许浮点类型甚至类类型的非类型模板参数。然而,后者有很多限制,本文不再进一步讨论。

在 Grid 类模板中,你可以使用非类型模板参数来指定网格的高度和宽度,而不是在构造函数中指定它们。在模板列表中而不是在构造函数中指定非类型参数的主要优点是在编译代码之前这些值是已知的。回想一下,编译器通过在编译前替换模板参数来为模板实例化生成代码。因此,您可以在实现中使用普通的二维数组,而不是动态调整大小的向量向量。这是新的类定义:

export template <typename T, size_t WIDTH, size_t HEIGHT>
class Grid {
  public:
  	Grid() = default;
  	virtual ~Grid() = default;
  	
	// Explicitly default a copy constructor and assignment operator.
	Grid(const Grid& src) = default;
	Grid& operator=(const Grid& rhs) = default;
	
	std::optional<T>& at(size_t x, size_t y);
	const std::optional<T>& at(size_t x, size_t y) const;
	
	size_t getHeight() const { return HEIGHT; }
	size_t getWidth() const { return WIDTH; }
	
  private:
  	void verifyCoordinate(size_t x, size_t y) const;
  	std::optional<T> m_cells[WIDTH][HEIGHT];
};

此类没有显式默认移动构造函数和移动赋值运算符,因为 C 样式数组无论如何都不支持移动语义。

注意,模板参数列表需要三个参数:网格中存储的对象类型,以及网格的宽高。宽度和高度用于创建一个二维数组来存储对象。

不能使用非常量整数来指定高度或宽度。以下代码无法编译:

size_t height { 10 };
Grid<int, 10, height> testGrid;

但是,如果将高度定义为常量,它会编译:

const size_t height { 10 };
Grid<int, 10, height> testGrid;

具有正确返回类型的 constexpr 函数也可以工作。例如,如果您有一个返回 size_t 的 constexpr 函数,可以使用它来初始化高度模板参数:

constexpr size_t getHeight() { return 10; }
...
Grid<double, 2, getHeight()> myDoubleGrid;

现在宽度和高度是模板参数,它们是每个网格类型的一部分。这意味着 Grid 和 Grid 是两种不同的类型。你不能将一种类型的对象分配给另一种类型的对象,并且一种类型的变量不能传递给需要另一种类型变量的函数或方法。

你可能感兴趣的:(C++,c++,开发语言)