5

C++类模板特化与继承使用说明书,新手也能get

 6 months ago
source link: https://www.51cto.com/article/777128.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C++类模板特化与继承使用说明书,新手也能get

作者:未平 2023-12-20 15:48:51
你可以为特定类型提供类模板的替代实现。例如,你可能认为 const char* 类型(C 风格字符串)的 Grid 行为没有意义。

一、类模板特化

1.特化的实现

你可以为特定类型提供类模板的替代实现。例如,你可能认为 const char 类型(C 风格字符串)的 Grid 行为没有意义。Grid<const char> 将在 vector<vector<optional<const char*>>> 中存储其元素。拷贝构造函数和赋值运算符将执行这些 const char 指针类型的浅拷贝。对于 const char,进行深拷贝字符串可能更有意义。最简单的解决方案是为 const char 编写一个专门的实现,将它们转换为 C++ 字符串,并存储在 vector<vector<optional<string>>> 中。

888bf7b55c71205254c323f8ff0ec9fb60bc45.jpg

模板的替代实现称为模板特化。你可能会发现其语法初看有些奇怪。当你编写类模板特化时,你必须指定这是模板,并且你正在为特定类型编写模板的版本。以下是 Grid 的 const char

export module grid:string;

// 当使用模板特化时,原始模板也必须可见。
import :main;

export template <>
class Grid<const char*> {
public:
    explicit Grid(size_t width = DefaultWidth, size_t height = DefaultHeight);
    virtual ~Grid() = default;
    // 明确默认拷贝构造函数和赋值运算符。
    Grid(const Grid& src) = default;
    Grid& operator=(const Grid& rhs) = default;
    // 明确默认移动构造函数和赋值运算符。
    Grid(Grid&& src) = default;
    Grid& operator=(Grid&& rhs) = default;

    std::optional<std::string>& at(size_t x, size_t y);
    const std::optional<std::string>& at(size_t x, size_t y) const;

    size_t getHeight() const { return m_height; }
    size_t getWidth() const { return m_width; }

    static const size_t DefaultWidth { 10 };
    static const size_t DefaultHeight { 10 };

private:
    void verifyCoordinate(size_t x, size_t y) const;
    std::vector<std::vector<std::optional<std::string>>> m_cells;
    size_t m_width { 0 }, m_height { 0 };
};

注意,在特化中你不使用任何类型变量,例如 T,你直接使用 const char* 和字符串。此时一个明显的问题是,为什么这个类仍然是模板。即,以下语法有什么用途?

template <> class Grid<const char*>

这种语法告诉编译器,这个类是 Grid 类的 const char* 特化。假设你没有使用这种语法,而是尝试编写如下代码:

class Grid

编译器不会允许你这样做,因为已经存在一个名为 Grid 的类模板(原始类模板)。只有通过特化,你才能重用这个名称。特化的主要好处是它们对用户来说可以是不可见的。当用户创建 int 或 SpreadsheetCells 的 Grid 时,编译器会从原始 Grid 模板生成代码。当用户创建 const char* 的 Grid 时,编译器使用 const char* 特化。这一切都可以在“幕后”进行。

2.主模块接口文件

主模块接口文件简单地导入并导出两个模块接口分区:

export module grid;
export import :main;
export import :string;

特化可以按照以下方式进行测试:

Grid<int> myIntGrid; // 使用原始 Grid 模板。
Grid<const char*> stringGrid1 { 2, 2 }; // 使用 const char* 特化。
const char* dummy { "dummy" };
stringGrid1.at(0, 0) = "hello";
stringGrid1.at(0, 1) = dummy;
stringGrid1.at(1, 0) = dummy;
stringGrid1.at(1, 1) = "there";
Grid<const char*> stringGrid2 { stringGrid1 };

当你特化一个模板时,你不会“继承”任何代码;特化不像派生。你必须重写类的整个实现。没有要求你提供具有相同名称或行为的方法。例如,const char* 的 Grid 特化实现了 at() 方法,返回 optional<string>,而不是 optional<const char*>。事实上,你可以编写一个与原始类完全不相关的完全不同的类。当然,这会滥用模板特化功能,如果没有充分理由,你不应该这样做。

下面是 const char* 特化的方法实现。与模板定义中不同,你不需要在每个方法定义前重复 template<> 语法。

Grid<const char*>::Grid(size_t width, size_t height)
    : m_width { width }, m_height { height } {
    m_cells.resize(m_width);
    for (auto& column : m_cells) {
        column.resize(m_height);
    }
}

void Grid<const char*>::verifyCoordinate(size_t x, size_t y) const {
    if (x >= m_width) {
        throw std::out_of_range { std::format("{} must be less than {}.", x, m_width) };
    }
    if (y >= m_height) {
        throw std::out_of_range { std::format("{} must be less than {}.", y, m_height) };
    }
}

const std::optional<std::string>& Grid<const char*>::at(size_t x, size_t y) const {
    verifyCoordinate(x, y);
    return m_cells[x][y];
}

std::optional<std::string>& Grid<const char*>::at(size_t x, size_t y) {
    return const_cast<std::optional<std::string>&>(std::as_const(*this).at(x, y));
}

二、从类模板派生

派生自类模板

您可以从类模板继承。如果派生类从模板本身继承,它也必须是一个模板。另外,您可以从类模板的特定实例继承,在这种情况下,您的派生类不需要是一个模板。

作为前者的一个例子,假设您决定通用的 Grid 类没有提供足够的功能来用作游戏棋盘。具体来说,您希望为游戏棋盘添加一个 move() 方法,将棋子从棋盘上的一个位置移动到另一个位置。以下是 GameBoard 模板的类定义:

import grid;

export template <typename T>
class GameBoard : public Grid<T> {
public:
    explicit GameBoard(size_t width = Grid<T>::DefaultWidth, size_t height = Grid<T>::DefaultHeight);
    void move(size_t xSrc, size_t ySrc, size_t xDest, size_t yDest);
};

这个 GameBoard 模板派生自 Grid 模板,从而继承了所有其功能。您不需要重写 at()、getHeight() 或任何其他方法。您也不需要添加拷贝构造函数、operator= 或析构函数,因为您在 GameBoard 中没有任何动态分配的内存。继承语法看起来很正常,除了基类是 Grid<T>,而不是 Grid。这种语法的原因是 GameBoard 模板并不真正从通用的 Grid 模板派生。相反,GameBoard 模板的每个实例化都派生自相同类型的 Grid 实例化。

例如,如果您使用 ChessPiece 类型实例化一个 GameBoard,那么编译器也会为 Grid<ChessPiece> 生成代码。: public Grid<T> 语法表示这个类继承自对于 T 类型参数有意义的任何 Grid 实例化。请注意,尽管一些编译器不强制执行,但 C++ 名称查找规则要求您使用 this 指针或 Grid<T>:: 来引用基类模板中的数据成员和方法。因此,我们使用 Grid<T>::DefaultWidth 而不是仅仅使用 DefaultWidth。以下是构造函数和 move() 方法的实现:

template <typename T>
GameBoard<T>::GameBoard(size_t width, size_t height) : Grid<T> { width, height } { }

template <typename T>
void GameBoard<T>::move(size_t xSrc, size_t ySrc, size_t xDest, size_t yDest) {
    Grid<T>::at(xDest, yDest) = std::move(Grid<T>::at(xSrc, ySrc));
    Grid<T>::at(xSrc, ySrc).reset(); // 重置源单元
    // 或者:
    // this->at(xDest, yDest) = std::move(this->at(xSrc, ySrc));
    // this->at(xSrc, ySrc).reset();
}

您可以按以下方式使用 GameBoard 模板:

GameBoard<ChessPiece> chessboard { 8, 8 };
ChessPiece pawn;
chessBoard.at(0, 0) = pawn;
chessBoard.move(0, 0, 0, 1);

注意:当然,如果您想重写 Grid 中的方法,您必须在 Grid 类模板中将它们标记为虚拟的。

三、继承与特化

代码复用?

是:派生类包含所有基类的数据成员和方法。

否:您必须在特化中重写所有所需代码。

名称复用?

否:派生类名称必须与基类名称不同。

是:特化必须与原始模板具有相同的名称。

支持多态性?

是:派生类的对象可以代替基类的对象。

否:每个类型的模板实例化都是不同的类型。

注意:使用继承来扩展实现和实现多态性。使用特化来为特定类型定制实现。

责任编辑:赵宁宁 来源: coding日记

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK