我有一个 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++智能指针之一,它在那里,因此我们不必手动处理分配。由于程序员分心,在代码中使用newdelete可能会导致内存泄漏和灾难,因此智能指针使此问题不复存在。

最后,为了退回物品,您可以简单地将物品放回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一样,变体将在模板参数中具有其可能的类型作为参数(即SwordBottle类型是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,这意味着您可以基于构造函数更改模板结构的模板参数。

10-04 17:51