C++ 允许对类的单个方法进行模板化。这种方法被称为方法模板,可以存在于普通类或类模板中。编写方法模板实际上就是为许多不同类型编写该方法的不同版本。方法模板对于类模板中的赋值运算符和拷贝构造函数非常有用。
警告:虚方法和析构函数不能是方法模板。
考虑仅有一个模板参数的原始 Grid 模板:元素类型。您可以实例化许多不同类型的网格,例如 int 和 double:
Grid myIntGrid;
Grid myDoubleGrid;
然而,Grid
myDoubleGrid = myIntGrid; // 无法编译
Grid newDoubleGrid { myIntGrid }; // 无法编译
问题在于 Grid 模板的拷贝构造函数和赋值运算符定义如下:
Grid(const Grid& src);
Grid& operator=(const Grid& rhs);
等效于:
Grid(const Grid& src);
Grid& operator=(const Grid& rhs);
Grid 的拷贝构造函数和 operator= 都需要一个 const Grid
Grid(const Grid& src);
Grid& operator=(const Grid& rhs);
注意,生成的 Grid
幸运的是,您可以通过向 Grid 类添加模板化的拷贝构造函数和赋值运算符的版本来纠正这种疏忽,从而生成将一个网格类型转换为另一个网格类型的方法。以下是新的 Grid 类模板定义:
export template
class Grid {
public:
template
Grid(const Grid& src);
template
Grid& operator=(const Grid& rhs);
void swap(Grid& other) noexcept;
// 为了简洁省略部分内容
};
原始的拷贝构造函数和拷贝赋值运算符不能被移除。如果 E 等于 T,编译器不会调用这些新的模板化拷贝构造函数和模板化拷贝赋值运算符。首先查看新的模板化拷贝构造函数:
template
Grid(const Grid& src);
您可以看到有另一个模板声明,使用不同的类型名 E(代表“元素”)。类在一个类型 T 上进行模板化,新的拷贝构造函数也在不同的类型 E 上进行模板化。这种双重模板化允许您将一个类型的网格复制到另一个类型。以下是新拷贝构造函数的定义:
template
template
Grid::Grid(const Grid& src)
: Grid { src.getWidth(), src.getHeight() } {
// 此构造函数的 ctor-initializer 首先委托给非拷贝构造函数来分配适当的内存量。
// 下一步是复制数据。
for (size_t i { 0 }; i < m_width; i++) {
for (size_t j { 0 }; j < m_height; j++) {
m_cells[i][j] = src.at(i, j);
}
}
}
如您所见,您必须在成员模板行(带 E 参数)之前声明类模板行(带 T 参数)。您不能像这样组合它们:
template // 对于嵌套模板构造函数错误!
Grid::Grid(const Grid& src)
除了构造函数定义之前的额外模板参数行外,注意您必须使用公共访问方法 getWidth()、getHeight() 和 at() 来访问 src 的元素。那是因为您正在复制到的对象是 Grid
swap() 方法实现如下:
template
void Grid::swap(Grid& other) noexcept {
std::swap(m_width, other.m_width);
std::swap(m_height, other.m_height);
std::swap(m_cells, other.m_cells);
}
模板化赋值运算符接受一个 const Grid
template
template
Grid& Grid::operator=(const Grid& rhs) {
// 使用复制-交换习惯用法
Grid temp { rhs }; // 在临时实例中完成所有工作。
swap(temp); // 仅通过非抛出操作提交工作。
return *this;
}
这个赋值运算符的实现使用了复制-交换习惯用法。swap() 方法只能交换同一类型的 Grids,但这没关系,因为这个模板化赋值运算符首先使用模板化拷贝构造函数将给定的 Grid
在先前的例子中,使用整数模板参数 HEIGHT 和 WIDTH,主要问题是高度和宽度成为了类型的一部分。这种限制阻止了将一个尺寸的网格赋值给另一个不同尺寸的网格。然而,在某些情况下,将一个大小的网格赋值或拷贝给不同大小的网格是可取的。与其使目标对象成为源对象的完美克隆,不如只从源数组中复制适合目标数组的元素,并在源数组较小的维度上用默认值填充目标数组。使用赋值运算符和拷贝构造函数的方法模板,您可以做到这一点,从而允许赋值和拷贝不同大小的网格。以下是类定义:
export template
class Grid {
public:
Grid() = default;
virtual ~Grid() = default;
// 明确默认拷贝构造函数和赋值运算符。
Grid(const Grid& src) = default;
Grid& operator=(const Grid& rhs) = default;
template
Grid(const Grid& src);
template
Grid& operator=(const Grid& rhs);
void swap(Grid& other) noexcept;
std::optional& at(size_t x, size_t y);
const std::optional& 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 m_cells[WIDTH][HEIGHT];
};
这个新定义包括拷贝构造函数和赋值运算符的方法模板,以及一个辅助方法 swap()。注意,非模板化的拷贝构造函数和赋值运算符是明确默认的(因为用户声明了析构函数)。它们仅复制或赋值源对象的 m_cells 到目标对象,这正是对于相同大小的两个网格所希望的语义。
下面是模板化拷贝构造函数的实现:
template
template
Grid::Grid(const Grid& src) {
for (size_t i { 0 }; i < WIDTH; i++) {
for (size_t j { 0 }; j < HEIGHT; j++) {
if (i < WIDTH2 && j < HEIGHT2) {
m_cells[i][j] = src.at(i, j);
} else {
m_cells[i][j].reset();
}
}
}
}
请注意,此拷贝构造函数仅从 src 中复制 x 和 y 维度上的 WIDTH 和 HEIGHT 元素,即使 src 比这更大。如果 src 在任一维度上较小,则额外位置的 std::optional 对象使用 reset() 方法重置。
下面是 swap() 方法和赋值运算符 operator= 的实现:
template
void Grid::swap(Grid& other) noexcept {
std::swap(m_cells, other.m_cells);
}
template
template
Grid& Grid::operator=(
const Grid& rhs) {
// 使用复制-交换习惯用法
Grid temp { rhs }; // 在临时实例中完成所有工作。
swap(temp); // 仅通过非抛出操作提交工作。
return *this;
}
这个赋值运算符的实现使用了复制-交换习惯用法。swap() 方法只能交换相同类型的 Grids,但这是可以的,因为这个模板化赋值运算符首先使用模板化拷贝构造函数将给定的 Grid