我有一个 list 类,在其中我想用Sword,Shield和Potion类的对象组成一个数组。
class Inventory {
public:
Inventory();
~Inventory();
virtual void add();
Inventory** getinvent();
void setinvent(Inventory** new_inventory);
int getsize();
void setsize(int new_size);
private:
Inventory** inventory;
int invent_size;
};
Inventory::Inventory() {
inventory = new Inventory*[1];
invent_size = 1;
}
class Sword : public Inventory {
public:
Sword(int strength);
~Sword();
void add();
private:
int strength;
Sword* sword;
};
Sword::Sword(int strength) {
this->strength = strength;
sword = this;
}
void Sword::add() {
setsize(getsize() + 1);
Inventory** new_invent = new Inventory*[getsize()];
for (int i = 0; i < getsize() - 1; i++) {
new_invent[i] = getinvent()[i];
}
new_invent[getsize() - 1] = sword;
setinvent(new_invent);
}
Shield和Potion类类似于Sword类。如果我在实现中创建以下对象:
Inventory* inventory = new Inventory();
Sword* sword = new Sword(1);
现在如何将这把剑添加到此特定 list 中?我不认为剑->添加();会起作用,因为剑不知道它是从库存继承而来的。它是否正确?
我试图使add()方法虚拟化,因为它必须适用于剑,盾和药水对象。
最佳答案
使用动态多态性,我们可以创建抽象类Item
,它描述物品在库存中具有的功能。之所以有用,是因为通过此类,可以管理我们不知道的项目,我们只知道它们的行为就像一个项目。
class Item
{
public:
virtual ~Item() = default;
virtual const char* description() const = 0;
};
更进一步,所有其他项目(剑,瓶等)都可以从此类继承,从而赋予它们成为项目的特征:
class Sword: public Item
{
public:
Sword() = default;
virtual ~Sword() = default;
const char* description() const override
{ return "Sword"; }
};
在
description
方法中,它覆盖了Item::description
抽象方法,因此,每当您从.description
实例调用Sword
时,都将返回"Sword"
字符串。例如:Sword sword{};
Item& item = sword;
std::puts(item.description()); // prints the "Sword" string.
现在,存储项目更加简单,我们只需要使用它们的 vector 即可:
std::vector<std::unique_ptr<Item>>
。#include <vector>
#include <memory>
std::vector<std::unique_ptr<Item>> inventory{};
inventory.emplace_back(std::make_unique<Sword>());
但是为什么我们不能使用
std::vector<Item>
?仅仅因为不可能从Item
构造Sword
。实际上,甚至不可能构造Item
,因为它具有抽象方法(即它们仅用于描述方法的原型(prototype),而不是其定义/实现)。std::unique_ptr
是为数不多的C++智能指针之一,它在那里,因此我们不必手动处理分配。由于程序员分心,在代码中使用new
和delete
可能会导致内存泄漏和灾难,因此智能指针使此问题不复存在。最后,为了退回物品,您可以简单地将物品放回Sword中:
const auto& item = inventory[0]; // item is `const std::unique_ptr<Item>&`
puts(item->description()); // prints "Sword"
puts(dynamic_cast<Sword*>(item.get())->description()); // also prints "Sword"
后者(使用dynamic_cast)将从
item.get()
method创建指向该第一项的转换指针,但形式为Sword*
。如果Sword
中存在与Item
不共通的方法或数据成员,则需要执行此操作。例如,如果您有类似“int sword_power`”的代码,则可以这样做:auto sword = dynamic_cast<Sword*>(item.get());
if (sword != nullptr)
{
std::printf("sword power: %d\n", sword->sword_power);
}
当然,检查强制转换是否成功是可选的,但是这样做可以防止您的代码执行未定义的行为(以防强制转换失败并返回空指针)。
还有另一种使用新的库工具
std::variant
来完成此系统的方法(C++ 17之前为而非)。基本上,变体可以让您一次拥有许多不同类型中的一种。与元组不同,元组可以让您拥有许多不同的类型(例如struct),而变体一次只能让一种类型的一个值。为了更好地理解它,这是它的工作原理:
#include <variant> // C++17
struct Sword {};
struct Bottle {};
std::variant<Sword, Bottle> item = Sword{};
像
std::tuple
一样,变体将在模板参数中具有其可能的类型作为参数(即Sword
和Bottle
类型是item
整个类型的一部分)。这样,您可以一次拥有一把剑或一瓶酒,但是永远不会同时拥有。让我们使用该新功能实现 list 。首先,我们必须对类进行一些更改:class Sword
{
public:
int power;
Sword() = default;
const char* description() const
{ return "Sword"; }
};
class Bottle
{
public:
bool empty;
Bottle() = default;
const char* description() const
{ return "Bottle"; }
};
我们消除了对虚拟方法和动态多态性的需求,您将进一步看到我们不再需要动态分配,因为
std::variant
必须在堆栈中工作(这意味着程序也可能会更快)。现在,对于
Item
概念,我们为我们的类添加了variant的别名:using Item = std::variant<Sword, Bottle>;
我们也可以将其与 vector 一起使用:
std::vector<Item> inventory{};
inventory.emplace_back(Sword{});
inventory.emplace_back(Bottle{});
与这些项目进行交互有几种方法,以防您需要它们。一种是使用
std::holds_alternative
:auto& item = inventory[0];
if (std::holds_alternative<Sword>(item))
{
auto& sword = std::get<Sword>(item);
sword.power = 42;
std::printf("%s: %d\n", sword.description(), sword.power);
}
它检查变量的对象是否持有给定类型的值。在这种情况下,我们检查了
Sword
。然后,如果里面有剑,我们将使用std::get<>
来获取值,它会以Sword
的形式返回对我们商品的引用。访问真实对象的另一种方法是使用std::visit。简而言之:访问者是行为类似于带重载功能的对象。您可以像访问函数一样 call 访客。为了造访者,我们可以使用带有重载
operator()
或lambda的结构。这是第一种方法:struct VisitItem
{
void operator() (Sword& sword) const
{
std::printf("%s: %d\n", sword.description(), sword.power);
}
void operator() (Bottle& bottle) const
{
std::printf("%s: %s\n", bottle.description(),
bottle.empty? "empty" : "full");
}
};
auto& item = inventory[0];
std::visit(VisitItem{}, item); // we give an instance of VisitItem for std::visit, and the item itself.
在这里,
std::visit
将为变体(即商品)内的当前对象调用正确的operator()
。如果项目持有剑,则将调用operator() (Sword&)
。另一种方法是制作重载的lambda。有点复杂,因为我们没有为此提供的库工具,但是使用C++ 17实际上更容易实现:
template <typename... Ts>
struct overload : Ts...
{
using Ts::operator()...;
template <typename... TTs>
constexpr explicit overload(TTs&&... tts) noexcept
: Ts{std::forward<TTs>(tts)}...
{
}
};
template <typename... Ts>
explicit overload(Ts&&...) -> overload<std::decay_t<Ts>...>;
然后像这样使用它:
auto& item = inventory[0];
auto visitor = overload(
[] (Sword& s) { std::printf("power: %d\n", s.power); },
[] (Bottle& b) { std::printf("%s\n", b.empty? "empty" : "full"); }
);
std::visit(visitor, item);
如果您想了解
overload
结构中发生的事情,它是从您提供的所有lambda继承而来的,并将operator()
重载引入重载查找(因为基类的函数重载不被视为候选,因此必须using overload
)。 overload
结构后面的行是user-defined deduction guide,这意味着您可以基于构造函数更改模板结构的模板参数。