2.2.2、显式实例化
有危险存在于有些类模板成员函数的编译错误,在隐式实例化时没有注意到。未被使用的类模板成员函数也可能包含语法错误,因为它们不会被编译到。这会使得检测代码的语法错误很困难。可以强制编译器生成所有成员函数的代码,virtual与non-virtual,通过使用explicit template instantiations。举例如下:
template class Grid<string>;
注意:Explicit template instantiations有助于发现错误,因为它们强制所有的类模板成员函数进行编译,即使没有被使用。
当使用Explicit template instantiations,不要只是尝试实例化类模板的基本类型,比如int,要用更复杂的类型,比如string,如果类模板支持这些类型的话。
2.2.3、类型的模板要求
当书写类型无关的代码时,必须假定这些类型的特定场景。例如,在Grid灯模板中,假定元素类型(用T代表)是可被析构的,copy/move可构建的,copy/move可赋值的。
当编译器尝试用被调用的类模板成员函数不支持的操作实例化模板时,代码编译失败,错误代码通常无法辨识。然而,即使想要使用的类型不支持类模板的所有成员函数要求的操作,也可以开发选择性的实例化来使用一些成员函数而不是其它的。
可以使用concept来书写编译器可以解释与验证的模板参数需求。编译器可以生成更多可读的错误,如果模板参数传递给不满足要求的模板实例。concept我们在本章后面讨论。
2.3、在文件之间发布模板代码
对于类模板,类模板定义与成员函数定义必须在使用它们的任何源文件中对编译器可见。有几项技术可以完成这个要求。
2.3.1、成员函数定义在与类模板定义同一文件中
可以将成员函数定义直接放到定义类模板自身的模块接口文件中。当在另一个使用模板的源文件中导入这个模块时,编译器具有所有需要代码的访问权限。这项技术用在了前面 Grid的实现中。
2.3.2、成员函数定义在独立的文件中
换一种方式,可以将类模板成员函数定义放至独立的模块接口分区文件中。这样也需要将类模板定义放在自身的模块接口分区中。例如,Grid类模板的主模块接口文件可能看起来像这样:
export module grid;
export import :definition;
export import :implementation;
导入与导出两个模块接口分区:definition与implementation。类模板定义定义在了definition分区:
export module grid:definition;
import std;
export template <typename T> class Grid { ... };
成员函数的实现在implementation分区,也需要导入definition分区,因为它需要Grid类模板定义:
export module grid:implementation;import :definition;
import std;export template <typename T>
Grid<T>::Grid(std::size_t width, std::size_t height)
: m_width { width }, m_height { height }
{ /* ... */ }
// Remainder omitted for brevity.
2.4、模板参数
在Grid例子中,Grid类模板有一个模板参数:在网格中保存的类型。当书写类模板时,在尖括号中指定参数,如下:
template <typename T>
该参数与函数中的参数列表类似。与函数一样,可以书写想要的任意多的模板参数的类模板。还有,这些参数不强制为类型,可以有缺省值。
2.4.1、非类型模板参数
非类型模板参数是“正常”参数,如int与指针--从函数来的比较熟悉的参数类型。然而,非类型模板参数只能是整型(char,int,long,等等),枚举,指针,引用,std::nullptr_t,auto&,auto*,浮点型,与类类型。后面这种,然而,会有许多限制,本文不再深入讨论。记住模板在编译时实例化;因此,非类型模板参数在编译时验证。这意味着这样的参数必须是常量或编译时常数。
在Grid类模板中,可以使用非类型模板参数来指定网格的高度与宽度而不是在构造函数中指定。使用非类型模板参数而不使用构造函数参数的主要优势是在代码编译前其值已知。回想一下,在编译前通过替换模板参数,编译器生成模板实例的代码。这样,可以在下面的实现中使用正常的二维数组,而不是使用动态改变大小的vector来进行线性表示。下面是修改之后的新的类模板定义:
export
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
class Grid
{
public:Grid() = default;virtual ~Grid() = default;// Explicitly default a copy constructor and copy assignment operator.Grid(const Grid& src) = default;Grid& operator=(const Grid& rhs) = default;// Explicitly default a move constructor and move assignment operator.Grid(Grid&& src) = default;Grid& operator=(Grid&& rhs) = default;std::optional<T>& at(std::size_t x, std::size_t y);const std::optional<T>& at(std::size_t x, std::size_t y) const;std::size_t getHeight() const { return HEIGHT; }std::size_t getWidth() const { return WIDTH; }private:void verifyCoordinate(std::size_t x, std::size_t y) const;std::optional<T> m_cells[WIDTH][HEIGHT];
};
现在模板参数列表有三个参数了:保存在网格中的对象类型,网格的宽度与高度。宽度与高度用于生成二维数组保存对象。下面是类模板成员函数定义:
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
void Grid<T, WIDTH, HEIGHT>::verifyCoordinate(std::size_t x, std::size_t y) const
{if (x >= WIDTH) {throw std::out_of_range { std::format("x ({}) must be less than width ({}).", x, WIDTH) };}if (y >= HEIGHT) {throw std::out_of_range { std::format("y ({}) must be less than height ({}).", y, HEIGHT) };}
}template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
const std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(std::size_t x, std::size_t y) const
{verifyCoordinate(x, y);return m_cells[x][y];
}template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
std::optional<T>& Grid<T, WIDTH, HEIGHT>::at(std::size_t x, std::size_t y)
{return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));
}
注意先前指定Grid<T>的地方,现在要指定Grid<T,WIDTH,HEIGHT>来指定三个模板参数。
可以实例化该模板,使用如下:
Grid<int, 10, 10> myGrid;Grid<int, 10, 10> anotherGrid;myGrid.at(2, 3) = 42;anotherGrid = myGrid;println("{}", anotherGrid.at(2, 3).value_or(0));
代码看起来很棒,但是不幸的是,会有比你预期的限制更多。首先,不能用非常数的整数来指定高度与宽度。下面的代码编译不成功:
size_t height { 10 };
Grid<int, 10, height> testGrid; // DOES NOT COMPILE
如果定义height为常数,编译成功:
const size_t height { 10 };
Grid<int, 10, height> testGrid; // Compiles and works
带有正确返回类型的constexpr函数也没总是。例如,如果有一个constexpr函数返回一个size_t,可以用它来初始化模板的height参数:
constexpr size_t getHeight() { return 10; }
...
Grid<double, 2, getHeight()> myDoubleGrid;
第二个限制更要命。既然宽度与高度已经是模板参数了,它们就成为了每个网格类型的一部分。这意味着Grid<int,10,10>与Grid<int,10,11>是两种不同的类型。不能将一种类型的对象赋值给另一种类型的对象,也不能将一种类型的变量传递给期待另一种类型变量的函数。
注意:非类型模板参数成为了实例化对象的类型规格的一部分。