C++学习笔记----11、模块、头文件及各种主题(一)---- 模板概览与类模板(1)
1、模板概览
面向过程的范式中的主要编程单元是过程或函数。函数有用主要是因为它们允许写特定值的独立的算法,因而可以重用于许多不同的值。例如,C++中的sqrt()函数计算调用者提供的值的平方根。只计算一个数值,比如数字4的平方根的平方根函数不是特别有用!sqrt()函数使用参数,它可以是调用者传递的任何值。计算机科学家将之称为参数化值的函数。
面向对象的编程范式添加了对象的概念,它将相关的数据与行为分组,但是不改变函数与成员函数的参数化值的方式。
模板将参数化的概念更进一步,允许像值一样参数化类型。C++中的类型包含原始类型如int与double,也有用户定义的类如SpreadsheetCell与CherryTree。使用模板,可以书写不只给定的值是独立的,这些值的类型也是独立的。例如,不是书写独立的栈类来保存int,Car与SpreadsheetCell,可以书写一个栈类模板定义,可以用于这些类型的任意一种。
虽然模板是一个令人惊奇的语言特性,C++中的模板在语法上也会令人迷惑,因此,许多入口避免自己书写模板。然而,每一个专业C++程序员需要知道如何书写它们,并且每一个程序员至少需要知道如果使用模板,因为它们广泛用于库,比如C++标准库。
本章会教会你关于C++支持的模板,重点关注在标准库中应用的特性。跟随学习的路径,会学到一些实用的特性,除了使用标准库之外,也可以用于程序中。
2、类模板
类模板为类定义家族定义了一个蓝图(=模板),变量类型,成员函数的返回类型,和/或成员函数的参数被指定为模板类型参数。类模板就像建筑的蓝图。允许编译器通过用具体的类型替换模板类型参数来建造(也叫做实例化)具体的类定义。
类模板主要用于窗口,或者数据结构,来保存对象。在本博客的早期,你已经经常使用类模板了,比如std::vector,unique_ptr,string,等等。本节讨论如何通过使用运行Grid容器来书写自己的类模板。为了使例子在长度上说得通,又足够简单来展示特定的观点,不同的章节给Grid容器添加了特性,在接下来的章节中并不会使用。
2.1、书写一个类模板
假定你想要一个通用游戏棋盘类,可以用作国际象棋棋盘,跳棋棋盘、井子棋棋盘,或者任何其他二维的游戏棋盘。为了使其能够满足通用目的,需要能够保存国际象棋棋子,跳棋棋子,井子棋棋子,或任何游戏棋子。
2.1.1、不使用模板的代码
不使用模板,最好的方法是构建一个通用的游戏棋盘来应用多态去保存通用的GamePiece对象。然后,可以让每一个游戏的棋子继承GamePiece类。例如,在国际象棋中,ChessPiece会是GamePiece的继承类。通过多态,GameBoard,写来去保存GamePiece,也可以保存ChessPiece。因为它应该能够拷贝GameBoard,GameBoard需要能够拷贝GamePiece。这种实现使用了多态,所以一个解决方案需要添加一个纯的虚clone()成员函数到GamePiece基类。它的继承类必须实现来返回一个具体的GamePiece的拷贝。下面是基本的GamePiece接口:
export class GamePiece
{
public:
virtual ~GamePiece() = default;
virtual std::unique_ptr<GamePiece> clone() const = 0;
};
GamePiece是一个抽象基类。像ChessPiece这样的具体类,继承于GamePiece,实现clone()成员函数:
class ChessPiece : public GamePiece
{
public:
std::unique_ptr<GamePiece> clone() const override
{
// Call the copy constructor to copy this instance
return std::make_unique<ChessPiece>(*this);
}
};
GameBoard代表了一个二维的网格,所以保存GameBoard中的GamePiece的一个选项可以是unique_ptr的vectors的vector。然而,这并不是一个数据的优化的代表,因为在内存中的数据是分离的。最好是作为unique_ptr的vector线性保存GamePiece的表示。将一个二维的坐标,比如(x,y)转化为一个一维的位置在线性表示,可以通过使用x+y*width的公式来简单实现。
export class GameBoard
{
public:
explicit GameBoard(std::size_t width = DefaultWidth, std::size_t height = DefaultHeight);
GameBoard(const GameBoard& src); // copy constructor
virtual ~GameBoard() = default; // virtual defaulted destructor
GameBoard& operator=(const GameBoard& rhs); // assignment operator
// Explicitly default a move constructor and move assignment operator.
GameBoard(GameBoard&& src) = default;
GameBoard& operator=(GameBoard&& src) = default;
std::unique_ptr<GamePiece>& at(std::size_t x, std::size_t y);
const std::unique_ptr<GamePiece>& at(std::size_t x, std::size_t y) const;
std::size_t getHeight() const { return m_height; }
std::size_t getWidth() const { return m_width; }
static constexpr std::size_t DefaultWidth{ 10 };
static constexpr std::size_t DefaultHeight{ 10 };
void swap(GameBoard& other) noexcept;
private:
void verifyCoordinate(std::size_t x, std::size_t y) const;
std::vector<std::unique_ptr<GamePiece>> m_cells;
std::size_t m_width { 0 }, m_height { 0 };
};
export void swap(GameBoard& first, GameBoard& second) noexcept;
在这个实现中,at()返回一个给定位置的游戏棋子的引用,而不是棋子的拷贝。GameBoard作为一个二维数组的抽象,所以它应该提供数组访问语法,通过返回在任何位置的真实对象的引用,而不是该对象的拷贝。客户端代码不应该保存将来要用的引用,因为它可能会失效。例如,当m_cells vector需要进行尺寸变化。反过来,客户端代码应该在使用返回引用之前调用at()。这遵守了标准库vector类的设计逻辑。
注意:该实现提供了两个版本的at();一个返回了reference-to-non- const,而另一个返回了reference-to- const。
注意:(C++23)从c++23开始,能够为GameBoard类提供多维的下标操作符。通过提供这样的操作符,客户可以书写myGameBoard[x,y],而不是myGameBoard.at(x,y)来访问在位置(x,y)上的棋子。
下面是成员函数的定义,注意这个实现对于赋值操作符使用了copy-and-swap习语,Scott Meyers的const_cast()模式来避免代码重复。
GameBoard::GameBoard(size_t width, size_t height)
: m_width{ width }
, m_height{ height }
{
m_cells.resize(m_width * m_height);
}
GameBoard::GameBoard(const GameBoard& src)
: GameBoard{ src.m_width, src.m_height }
{
// The ctor-initializer of this constructor delegates first to the
// non-copy constructor to allocate the proper amount of memory.
// The next step is to copy the data.
for (size_t i{ 0 }; i < m_cells.size(); ++i) {
if (src.m_cells[i]) {
m_cells[i] = src.m_cells[i]->clone();
}
}
}
void GameBoard::verifyCoordinate(size_t x, size_t y) const
{
if (x >= m_width) {
throw out_of_range { format("x ({}) must be less than width ({}).", x, m_width) };
}
if (y >= m_height) {
throw out_of_range { format("y ({}) must be less than height ({}).", y, m_height) };
}
}
void GameBoard::swap(GameBoard& other) noexcept
{
std::swap(m_width, other.m_width);
std::swap(m_height, other.m_height);
std::swap(m_cells, other.m_cells);
}
void swap(GameBoard& first, GameBoard& second) noexcept
{
first.swap(second);
}
GameBoard& GameBoard::operator=(const GameBoard& rhs)
{
// Copy-and-swap idiom
GameBoard temp{ rhs }; // Do all the work in a temporary instance
swap(temp); // Commit the work with only non-throwing operations
return *this;
}
const unique_ptr<GamePiece>& GameBoard::at(size_t x, size_t y) const
{
verifyCoordinate(x, y);
return m_cells[x + y * m_width];
}
unique_ptr<GamePiece>& GameBoard::at(size_t x, size_t y)
{
return const_cast<unique_ptr<GamePiece>&>(as_const(*this).at(x, y));
}
该GameBoard类工作得很好:
GameBoard chessBoard { 8, 8 };
auto pawn { std::make_unique<ChessPiece>() };
chessBoard.at(0, 0) = std::move(pawn);
chessBoard.at(0, 1) = std::make_unique<ChessPiece>();
chessBoard.at(0, 1) = nullptr;