马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
2.1.2、模板Grid类
在上一节中的GameBoard很不错但还不敷。一个问题是不能用GameBoard来以值来保存元素;它保存的是指针。另一个更严肃的问题是,与类型安全相关。在GameBoard的每一个网格中保存了一个unique_ptr<GamePiece>。纵然是保存ChessPiece,当使用at()来请示一个特定的棋子时,会得到一个unique_ptr<GamePiece>。这就意味着不得不将查询的GamePiece向下转化为ChessPiece以便能够使用ChessPiece的特定功能。还有,无法阻止在GameBoard中混合各种不同的GamePiece继承来的对象。例如,假定不但有ChessPiece,还有TicTacToePiece:
- class TicTacToePiece : public GamePiece
- {
- public:
- std::unique_ptr<GamePiece> clone() const override
- {
- // Call the copy constructor to copy this instance
- return std::make_unique<TicTacToePiece>(*this);
- }
- };
复制代码 用上一节的多态解决方案,无法阻止在单个的游戏面板上保存井字棋棋子与国际象棋棋子:
- GameBoard gameBoard { 8, 8 };
- gameBoard.at(0, 0) = std::make_unique<ChessPiece>();
- gameBoard.at(0, 1) = std::make_unique<TicTacToePiece>();
复制代码 这个大问题就是不管怎么样都要记住在特定位置保存的是什么,以便调用at()时执行正确的向下转化。
GameBoard的一个缺点是它不能用于保存原始数据类型,好比int或double,由于在网格中的数据类型必须继承自GamePiece。
如果写一个通用的Grid类,能够用于保存ChessPiece,SpreadsheetCell,int,double,等等,会是非常好的。在C++中,可以通过写一个类模板来轮到,它是类定义的一个蓝图。在类模板中,数据类型还不明确。客户可以在想使用时通过指定类型来实例化模板。这叫做泛型编程。泛型编程最大的好处是类型安全。在实例化类定义与成员函数中使用的类型是详细的类型,而不是上一节中使用多态解决方案的基类类型中的抽象类型。
我们开始看如何书写这样的一个Grid类模板定义。
2.1.2.1、Grid类模板定义
要明白类模板,研究一下其语法是很有帮助的。下面的例子展示了如何修改GameBoard类生成一个参数化的Grid类模板。代码后会对语法细节举行解释。留意名字由GameBoard修改为了Grid。Grid也应该能用于原始数据类型,如int与double。这就是为什么优选使用值语法而不是与用于GameBoard实现的多态指针语法相比的多态,来实现该解决方案。m_cells容器保存了真实的对象,而不是指针。与指针语法相比使用值语法的向下转化,使得不能有真正的空网格。也就是说,网格必须要有值。而指针语法,空网格保存了Nullptr。幸运的是,std: ptional来救场了。它允许使用值语法,同时也有表现空网格的方法。
- export
- template <typename T>
- class Grid
- {
- public:
- explicit Grid(std::size_t width = DefaultWidth, std::size_t height = DefaultHeight);
- 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;
- 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 m_height; }
- std::size_t getWidth() const { return m_width; }
- static constexpr std::size_t DefaultWidth{ 10 };
- static constexpr std::size_t DefaultHeight{ 10 };
- private:
- void verifyCoordinate(std::size_t x, std::size_t y) const;
- std::vector<std::optional<T>> m_cells;
- std::size_t m_width { 0 }, m_height { 0 };
- };
复制代码 现在看到了完备的类模板定义,先看第一行。
- export template <typename T>
复制代码 第一行说明下面的类定义是一个类型T的模板,从模块中导出。“template <typename T>”部分叫做模板头。在C++中template与typename都是关键字。我们前面讨论过,模板”参数化”类型以与函数”参数化”值一样的方式工作。与在函数中代表调用者传递的参数一样使用参数名字,在模板中使用模板类型参数名字(好比T)来代表调用者传递的模板类型参数一样的类型。名字T没有什么特别的--可以使用想用的任意名字。传统意义上,当单个类型使用时,叫做T,但是这只是历史上的定名规范,就像调用整数数组索引用i与j一样。模板标识符对整个语句有效,在这个例子中是类模板定义。
留意:由于历史的原因,可以使用关键字class而不是typename来指定模板类型参数。这样的话,很多册本与程序中的使用语法就酿成了这样:template <class T>。然而,在上下文中使用class关键字会令人迷惑,由于它隐含的意思是类型必须是一个类,现实上不是这样的。类型可以是一个类,一个布局,一个联合,一个原始数据类型,如int或double,等等。为了避免这种迷惑,我们使用typename。
在前面的GameBoard类中,m_cells数据成员为指针vector,要求特别的代码来拷贝--就须要拷贝构造函数与拷贝赋值操作符。在Grid类中,m_cells是一个可选值的vector,以是编译器生成的拷贝构造函数与赋值操作符就可以了。然而,一旦有了用户声明的析构函数,编译器隐式生成的拷贝构造函数或拷贝赋值操作符就会过时,以是Grid类模板显式地对它们举行了缺省。也显式地缺省了move构造函数与move赋值操作符。下面是显式缺省的拷贝赋值操作符:
- Grid& operator=(const Grid& rhs) = default;
复制代码 可以看到,rhs参数的类型不再是const GameBoard&,而酿成了const Grid&。在类定义内,编译器在须要的地方解释Grid为Grid<T>,但如果你想,可以显式地使用Grid<T>:
- Grid<T>& operator=(const Grid<T>& rhs) = default;
复制代码 然而,在类定义之后,必须使用Grid<T>。当写一个类模板时,过去认为的类名(Grid)现在现实上酿成模板名。当想谈论现实的Grid类或类型时,不得不使用template ID,也就是说,Grid<T>,它是特定类型,好比int,SpreadsheetCell或ChessPiece的Grid类模板的实例化。
由于m_cells不再保存指针,而酿成了可选值,at()成员函数现在返回optional<T>而不是unique_ptr,也就是说optional可以有一个类型T的值,或者为空:
- 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;
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |