使用动态多态,我们可以创建一个抽象类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
抽象一个,所以只要你从Sword
的实例调用.description
,就会返回"Sword"
字符串。例如:
Sword sword{};
Item& item = sword;
std::puts(item.description()); // prints the "Sword" string.
它现在更简单的存储项目,我们只需要使用它们的载体: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>
?仅仅因为不可能从Sword
构建Item
。实际上,甚至不可能构建一个Item
,因为它具有抽象方法(即它们只是用来描述方法的原型,而不是它的定义/实现)。
std::unique_ptr
是少数C++智能指针之一,它在那里,所以我们不必手动处理分配。在代码中使用new
和delete
可能会导致内存泄漏和灾难,因为程序员的注意力分散,所以智能指针会导致此问题不存在。
最后,为了有一个项目回来,你可以简单地向下转换的东西回剑:
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
不常见的方法或数据成员,则需要执行此操作。例如,如果你有这样的事情“诠释sword_power`,你可以这样做:
auto sword = dynamic_cast<Sword*>(item.get());
if (sword != nullptr)
{
std::printf("sword power: %d\n", sword->sword_power);
}
当然,如果检查中投成功,是可选的,但这样做可以防止你的代码执行未定义行为(以情况下,转换失败并返回一个空指针)。
目前仍然在做这个系统(不之前,C++ 17),使用新的库工具std::variant
的另一种方式。
基本上,变种可以让你一次有许多不同的类型之一帽子可以让你有很多不同的类型(比如一个结构体),一个变体只允许一次从一个类型获取一个值。为了更好地理解它,这里是它的工作原理是:
#include <variant> // C++17
struct Sword {};
struct Bottle {};
std::variant<Sword, Bottle> item = Sword{};
像std::tuple
,一个变异体将具有其可能的类型时,模板参数作为参数(即Sword
和Bottle
类型item
的整体型的一部分) 。这样,你可以一次有一把剑或一瓶,但永远不会在同一时间。让我们用这个新功能来实现我们的库存。首先,我们必须改变我们的班了一下:
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
概念,我们做的变体的一个别名与我们的类:
using Item = std::variant<Sword, Bottle>;
而且我们可以用向量也使用此:
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。简而言之,访问者就像一个具有重载功能的对象。你可以像一个函数一样调用访问者。为了创建一个访问者,我们可以使用一个结构超载的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&)
。
另一种方法是制作超载的lambdas。这是一个有点复杂的是,因为我们没有为图书馆的工具,但与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,这意味着您可以根据构造函数更改模板结构的模板参数。
为什么这些项目从'库存'继承? – user463035818
这个设计似乎有一个基本问题。将商品添加到库存的唯一机制是'add()',它只能将商品添加到自身。作为库存的剑的概念很奇怪。 –
你的班级结构令人困惑 - “Sword' *是一个*库存”是怎么回事?为什么'Inventory'包含其他'Inventory'? – Quentin