C++学习笔记----11、模块、头文件及各种主题(一)---- 模板概览与类模板(6)
2.5.1、带有非类型的模板参数的成员函数模板
前面的HEIGHT与WIDTH整型模板参数的Grid类模板的主要问题是,高度与宽度变成了类型的一部分。这种限制妨碍 了将一个高度与宽度的网格赋值给一个不同高度与宽度的网格。在有些场景中,是需要赋值或拷贝一种大小的网格到一个不同大小的网格的。不是合目标对象成为源对象的完美克隆,而是只拷贝那些源数组中的元素匹配到目的数组中,如果源数组在任何维度上小于目标数组的话,用缺省省填充目标数组。用成员函数模板的赋值操作符与拷贝构造函数,可以精确地做到这一点,这就允许了赋值与拷贝不同大小的网格。下面是类定义:
export
template <typename T, std::size_t WIDTH = 10, std::size_t HEIGHT = 10>
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;
template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2>
Grid(const Grid<E, WIDTH2, HEIGHT2>& src);
template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2>
Grid& operator=(const Grid<E, WIDTH2, HEIGHT2>& rhs);
void swap(Grid& other) noexcept;
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];
};
新的定义包含了成员函数模板的拷贝构造函数与赋值操作符,加一个辅助成员函数swap()。注意非参数化的拷贝构造函数与赋值操作符显式地缺省(因为有用户声明的析构函数)。它们只是拷贝可赋值m_cells从源到目标,这正是想要的两个同样大小的网格的语法。
下面是参数化的拷贝构造函数:
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2>
Grid<T, WIDTH, HEIGHT>::Grid(const Grid<E, WIDTH2, HEIGHT2>& src)
{
for (std::size_t i{ 0 }; i < WIDTH; ++i) {
for (std::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();
}
}
}
}
注意拷贝构造函数只是拷贝了WIDTH与HEIGHT元素的x与y维度,各自地从src,甚至如果src比目标大。如果src在任一维度小于目标,std::optional对象在额外的点上使用reset()成员函数重置。
下面是swap()与operator=的实现:
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
void Grid<T, WIDTH, HEIGHT>::swap(Grid& other) noexcept
{
std::swap(m_cells, other.m_cells);
}
template <typename T, std::size_t WIDTH, std::size_t HEIGHT>
template <typename E, std::size_t WIDTH2, std::size_t HEIGHT2>
Grid<T, WIDTH, HEIGHT>& Grid<T, WIDTH, HEIGHT>::operator=(
const Grid<E, WIDTH2, HEIGHT2>& rhs)
{
// Copy-and-swap idiom
Grid<T, WIDTH, HEIGHT> temp{ rhs }; // Do all the work in a temporary instance.
swap(temp); // Commit the work with only non-throwing operations.
return *this;
}
2.5.2、使用带有Explicit对象参数的成员函数模板来避免代码重复(C++23)
我们运行的Grid类模板的例子带有一个单独的模板类型参数T,包含了at()成员函数的两个重载,const与non-const。提醒一下:
export template <typename T>
class Grid
{
public:
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;
// Remainder omitted for brevity
};
它们的实现使用了Scott Meyers的const_cast()模式来避免代码重复:
template <typename T>
const std::optional<T>& Grid<T>::at(std::size_t x, std::size_t y) const
{
verifyCoordinate(x, y);
return m_cells[x + y * m_width];
}
template <typename T>
std::optional<T>& Grid<T>::at(std::size_t x, std::size_t y)
{
return const_cast<std::optional<T>&>(std::as_const(*this).at(x, y));
}
虽然没有代码重复,仍然需要显式定义const与non-const重载。从C++23开始,可以使用explicit object parameter来避免必须显式提供两种重载。其秘诀在于将at()成员函数墨迹为成员函数模板,显式对象参数self的类型是其自身,是一个模板类型参数Self,这样就能自动推演了。这个特性叫做duducing this。下面是这样的一个声明:
export template <typename T>
class Grid
{
public:
template <typename Self>
auto&& at(this Self&& self, std::size_t x, std::size_t y);
// Remainder omitted for brevity
};
实现使用了传递引用Self&&;看下面的注意,这样的传递引用可以绑定Grid<T>&,const Grid<T>&,与Grid<T>&&。
注意:类型Self&&的引用只是一个传递引用,当它用作函数或成员函数模板的参数时,使用Self作为它的一个模板类型参数。如果类成员函数有一个Self&&参数,但是带有类的Self模板类型参数,并且不是成员函数自身,那么 Self&&就不是一个引用传递,只是一个右值引用。这是因为编译器开始处理带有Self&&参数的成员函数时,类模板参数Self早已解析成了具体的类型,例如int,并且在当时,该成员函数的参数类型也已经被替换成了int&&。
下面是实现。记住使用显式对象参数的成员函数体内,需要使用显式对象参数,在本例中是self,来得到访问对象的权限;没有this指针。
template <typename T>
template <typename Self>
auto&& Grid<T>::at(this Self&& self, std::size_t x, std::size_t y)
{
self.verifyCoordinate(x, y);
return std::forward_like<Self>(self.m_cells[x + y * self.m_width]);
}
实现使用了C++23中引入的std::forward_like<Self>(x)。它返回一个对x的引用,使用了与Self&&类似的属性。这样,由于m_cells的元素类型为optional<T>,有如下结论:
- 如果Self&&绑定了Grid<T>&,返回类型为optional<T>&。
- 如果Self&&绑定了const Grid<T>&,返回类型为const optional<T>&。
- 如果Self&&绑定了Grid<T>&&,返回类型为optional<T>&&。
总结一下,有了成员函数模板,显式对象参数,传递引用,与forward_like()的组合,就能够声明与定义单个成员函数来提供const与non-const的实例了。