玩转C++方法模板,编程技能秒提升

开发 前端
C++ 允许对类的单个方法进行模板化。这种方法被称为方法模板,可以存在于普通类或类模板中。

方法模板

C++ 中的方法模板

C++ 允许对类的单个方法进行模板化。这种方法被称为方法模板,可以存在于普通类或类模板中。编写方法模板实际上就是为许多不同类型编写该方法的不同版本。方法模板对于类模板中的赋值运算符和拷贝构造函数非常有用。

警告:虚方法和析构函数不能是方法模板。

考虑仅有一个模板参数的原始 Grid 模板:元素类型。您可以实例化许多不同类型的网格,例如 int 和 double:

Grid<int> myIntGrid;
Grid<double> myDoubleGrid;

然而,Grid<int> 和 Grid<double> 是两种不同的类型。如果你编写一个接受 Grid<double> 类型对象的函数,你不能传递 Grid<int>。即使你知道 int 网格的元素可以复制到 double 网格的元素中,因为 int 可以转换为 double,但你不能将 Grid<int> 类型的对象赋值给 Grid<double> 类型的对象,或从 Grid<int> 构造 Grid<double>。以下两行都无法编译:

myDoubleGrid = myIntGrid; // 无法编译
Grid<double> newDoubleGrid { myIntGrid }; // 无法编译

问题在于 Grid 模板的拷贝构造函数和赋值运算符定义如下:

Grid(const Grid& src);
Grid& operator=(const Grid& rhs);

等效于:

Grid(const Grid<T>& src);
Grid<T>& operator=(const Grid<T>& rhs);

Grid 的拷贝构造函数和 operator= 都需要一个 const Grid<T>& 的引用。当你实例化 Grid<double> 并尝试调用拷贝构造函数和 operator= 时,编译器生成以下原型的方法:

Grid(const Grid<double>& src);
Grid<double>& operator=(const Grid<double>& rhs);

注意,生成的 Grid<double> 类中没有接受 Grid<int> 的构造函数或 operator=。

幸运的是,您可以通过向 Grid 类添加模板化的拷贝构造函数和赋值运算符的版本来纠正这种疏忽,从而生成将一个网格类型转换为另一个网格类型的方法。以下是新的 Grid 类模板定义:

export template <typename T>
class Grid {
public:
    template <typename E>
    Grid(const Grid<E>& src);

    template <typename E>
    Grid& operator=(const Grid<E>& rhs);

    void swap(Grid& other) noexcept;
    // 为了简洁省略部分内容
};

原始的拷贝构造函数和拷贝赋值运算符不能被移除。如果 E 等于 T,编译器不会调用这些新的模板化拷贝构造函数和模板化拷贝赋值运算符。首先查看新的模板化拷贝构造函数:

template <typename E>
Grid(const Grid<E>& src);

您可以看到有另一个模板声明,使用不同的类型名 E(代表“元素”)。类在一个类型 T 上进行模板化,新的拷贝构造函数也在不同的类型 E 上进行模板化。这种双重模板化允许您将一个类型的网格复制到另一个类型。以下是新拷贝构造函数的定义:

template <typename T>
template <typename E>
Grid<T>::Grid(const Grid<E>& 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 <typename T, typename E> // 对于嵌套模板构造函数错误!
Grid<T>::Grid(const Grid<E>& src)

除了构造函数定义之前的额外模板参数行外,注意您必须使用公共访问方法 getWidth()、getHeight() 和 at() 来访问 src 的元素。那是因为您正在复制到的对象是 Grid<T> 类型的,而您正在复制的对象是 Grid<E> 类型的。它们不是同一类型,所以您必须使用公共方法。

swap() 方法实现如下:

template <typename T>
void Grid<T>::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<E>&,但返回一个 Grid<T>&:

template <typename T>
template <typename E>
Grid<T>& Grid<T>::operator=(const Grid<E>& rhs) {
    // 使用复制-交换习惯用法
    Grid<T> temp { rhs }; // 在临时实例中完成所有工作。
    swap(temp); // 仅通过非抛出操作提交工作。
    return *this;
}

这个赋值运算符的实现使用了复制-交换习惯用法。swap() 方法只能交换同一类型的 Grids,但这没关系,因为这个模板化赋值运算符首先使用模板化拷贝构造函数将给定的 Grid<E> 转换为 Grid<T>,名为 temp。之后,它使用 swap() 方法将这个临时的 Grid<T> 与 this(也是 Grid<T> 类型)交换。

使用非类型参数的方法模板

不同大小网格的赋值和拷贝

在先前的例子中,使用整数模板参数 HEIGHT 和 WIDTH,主要问题是高度和宽度成为了类型的一部分。这种限制阻止了将一个尺寸的网格赋值给另一个不同尺寸的网格。然而,在某些情况下,将一个大小的网格赋值或拷贝给不同大小的网格是可取的。与其使目标对象成为源对象的完美克隆,不如只从源数组中复制适合目标数组的元素,并在源数组较小的维度上用默认值填充目标数组。使用赋值运算符和拷贝构造函数的方法模板,您可以做到这一点,从而允许赋值和拷贝不同大小的网格。以下是类定义:

export template <typename T, size_t WIDTH = 10, size_t HEIGHT = 10>
class Grid {
public:
    Grid() = default;
    virtual ~Grid() = default;
    // 明确默认拷贝构造函数和赋值运算符。
    Grid(const Grid& src) = default;
    Grid& operator=(const Grid& rhs) = default;

    template <typename E, size_t WIDTH2, size_t HEIGHT2>
    Grid(const Grid<E, WIDTH2, HEIGHT2>& src);

    template <typename E, size_t WIDTH2, size_t HEIGHT2>
    Grid& operator=(const Grid<E, WIDTH2, HEIGHT2>& rhs);

    void swap(Grid& other) noexcept;

    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];
};

这个新定义包括拷贝构造函数和赋值运算符的方法模板,以及一个辅助方法 swap()。注意,非模板化的拷贝构造函数和赋值运算符是明确默认的(因为用户声明了析构函数)。它们仅复制或赋值源对象的 m_cells 到目标对象,这正是对于相同大小的两个网格所希望的语义。

下面是模板化拷贝构造函数的实现:

template <typename T, size_t WIDTH, size_t HEIGHT>
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid<T, WIDTH, HEIGHT>::Grid(const Grid<E, WIDTH2, HEIGHT2>& 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 <typename T, size_t WIDTH, size_t HEIGHT>
void Grid<T, WIDTH, HEIGHT>::swap(Grid& other) noexcept {
    std::swap(m_cells, other.m_cells);
}

template <typename T, size_t WIDTH, size_t HEIGHT>
template <typename E, size_t WIDTH2, size_t HEIGHT2>
Grid<T,

 WIDTH, HEIGHT>& Grid<T, WIDTH, HEIGHT>::operator=(
    const Grid<E, WIDTH2, HEIGHT2>& rhs) {
    // 使用复制-交换习惯用法
    Grid<T, WIDTH, HEIGHT> temp { rhs }; // 在临时实例中完成所有工作。
    swap(temp); // 仅通过非抛出操作提交工作。
    return *this;
}

这个赋值运算符的实现使用了复制-交换习惯用法。swap() 方法只能交换相同类型的 Grids,但这是可以的,因为这个模板化赋值运算符首先使用模板化拷贝构造函数将给定的 Grid<E, WIDTH2, HEIGHT2> 转换为 Grid<T, WIDTH, HEIGHT>,称为 temp。之后,它使用 swap() 方法交换这个临时 Grid<T, WIDTH, HEIGHT> 和 this。

责任编辑:赵宁宁 来源: coding日记
相关推荐

2010-01-22 14:46:25

C++语言

2010-02-02 09:49:02

C++模板

2023-09-25 13:28:14

C++Lambda

2022-01-17 10:12:47

C++‘模板元编程

2010-01-13 18:34:43

C++ 托管

2023-12-18 11:15:03

2011-05-30 15:29:32

C++

2023-12-13 10:51:49

C++函数模板编程

2011-07-10 15:26:54

C++

2020-12-28 08:46:38

编程开发技能

2020-10-29 12:55:47

编程代码开发

2020-04-06 12:20:51

Vim文本编辑器Linux

2010-01-26 17:11:13

C++编程

2010-02-03 17:42:33

C++模板参数

2023-12-06 13:48:00

C++代码

2010-02-06 16:59:19

C++ kmp算法模板

2010-01-26 13:55:07

C++标准模板库

2010-01-11 14:13:03

C++学习方法

2022-09-22 10:22:36

C++编程语言代码

2010-01-19 18:18:21

C++语言
点赞
收藏

51CTO技术栈公众号