【问题标题】:How do you add abstract base class unique_ptrs to a map?如何将抽象基类 unique_ptrs 添加到地图中?
【发布时间】:2018-06-28 03:04:29
【问题描述】:

我目前正在用 C++ 编写游戏。这个游戏有一个 GameManager 类。 GameManager 类包含一个包含指向游戏对象的指针的映射。我已经定义了一个 GameObject 类,它是一个抽象类,仅充当接口。

我定义了两个派生自 GameObject 类的类:Enemy 和 Loot。

我希望我的 GameManager 类包含游戏对象的映射,或者更确切地说,包含指向游戏对象的指针。因为我的 GameManager 拥有这些对象,所以我希望地图包含 std::unique_ptr's。

但是,我很难将派生对象(例如敌人和战利品)实际添加到此地图。

我希望我的 GameManager 遍历游戏对象并调用抽象方法。本质上,我的 GameManager 并不关心某物是敌人、战利品还是其他什么,它只是希望能够调用基类中声明的“draw”方法。

如何将指向派生类的 unique_ptr 添加到包含基类的 unique_ptr 的映射?到目前为止,我的尝试导致我无法编译代码。我不断收到一条错误消息,指出我不允许将派生类指针动态转换为基类指针。

如果我使用原始指针,我觉得这很好,但我打算使用智能指针。

代码:

#include <memory>
#include <map>

class GameObject
{
public:
    virtual void draw() = 0;
};

class  Enemy : GameObject
{
public:
    void draw() {};
};

class  Loot : GameObject
{
public:
    void draw() {};
};

int main()
{
    std::map<int, std::unique_ptr<GameObject>> my_map;

    // How do I add an Enemy or Loot object unique_ptr to this map?
    my_map[0] = dynamic_cast<GameObject>(std::unique_ptr<Enemy>().get()); // doesn't compile, complains about being unable to cast to abstract class

    return 0;
}

【问题讨论】:

  • 请显示您迄今为止的尝试之一和错误消息。答案可能出人意料地简单,但为了使其易于理解,了解您来自哪里会有所帮助。
  • my_map.emplace(0, std::make_unique());
  • @JaMiT 我已经更新了。
  • 谢谢。看起来比我预期的要长一点的解释是有道理的。

标签: c++ inheritance smart-pointers unique-ptr


【解决方案1】:

错误消息的第一个原因是类类型永远不能用作dynamic_cast 的类型。 dynamic_cast 的目标类型必须始终是指向类类型的指针(意味着如果转换失败则结果为 null)或对类类型的引用(意味着如果转换失败则抛出异常)。

所以改进#1:

my_map[0] = dynamic_cast<GameObject*>(std::unique_ptr<Enemy>().get());

但这不起作用,因为GameObjectEnemy 的私有基类。您可能打算使用公共继承,但是(当使用 class 而不是 struct 时)您必须这样说:

class Enemy : public GameObject
// ...

接下来我们会发现map语句中的=是无效的。左侧的类型为std::unique_ptr&lt;GameObject&gt;,它没有任何可以采用GameObject* 指针的operator=。但它确实有一个 reset 成员用于设置原始指针:

my_map[0].reset(dynamic_cast<GameObject*>(std::unique_ptr<Enemy>().get()));

现在该语句应该可以编译了 - 但它仍然是错误的。

在找出错误的原因之前,我们可以做一个简化。 dynamic_cast 用于从指向基类的指针中获取指向派生类的指针,或者用于更复杂的继承树中的许多其他类型更改。但是根本不需要从指向派生类的指针中获取指向基类的指针:这是一种有效的隐式转换,因为每个具有派生类类型的对象都必须始终包含基类类型的子对象,并且没有 "失败”的案例。所以这里的dynamic_cast 可以直接去掉。

my_map[0].reset(std::unique_ptr<Enemy>().get());

下一个问题是std::unique_ptr&lt;Enemy&gt;() 创建了一个空的unique_ptr,而根本没有创建Enemy 对象。要创建一个实际的Enemy,我们可以改写std::unique_ptr&lt;Enemy&gt;(new Enemy)std::make_unique&lt;Enemy&gt;()

my_map[0].reset(std::make_unique<Enemy>().get());

仍然是错误的,而且方式有点棘手。现在的问题是创建的Enemy 对象归make_unique 返回的临时std::unique_ptr&lt;Enemy&gt; 对象所有。 reset 告诉映射中的std::unique_ptr&lt;GameObject&gt; 它应该拥有指向同一对象的指针。但在语句结束时,临时的std::unique_ptr&lt;Enemy&gt; 被销毁,它销毁了Enemy 对象。所以地图留下了一个指向死对象的指针,这是无效的 - 不是你想要的。

但这里的解决方案是我们根本不需要搞乱get()reset()。有一个operator= 允许将右值std::unique_ptr&lt;Enemy&gt; 分配给std::unique_ptr&lt;GameObject&gt;,它在这里做了正确的事情。它利用了从Enemy*GameObject* 的隐式转换。

my_map[0] = std::make_unique<Enemy>();

(请注意,如果您有一个 named std::unique_ptr&lt;Enemy&gt;,则需要 std::move 以允许分配,如 my_map[0] = std::move(enemy_ptr);。但上面不需要 std::move,因为make_unique 的结果已经是一个右值。)

现在这个语句更短更清晰,并且实际上会做你想做的。

一条评论也暗示了这种可能性

my_map.emplace(0, std::make_unique<Enemy>());

这也是有效的,但可能有一个重要的区别:如果地图已经有一个密钥为零的对象,= 版本将销毁并替换旧的,但emplace 版本将不理会地图而刚刚创建的Enemy 将被销毁。

【讨论】:

  • 我现在明白为什么存在像 Scott Meyers 这样的人了。
【解决方案2】:

dynamic_cast 只能用于指针和引用之间的转换。 GameObject 既不是指针类型,也不是引用类型,所以你不能dynamic_cast

您可能打算改用dynamic_cast&lt;GameObject*&gt;。但是,您不应该将dynamic_cast 指向(指向)基类的(指针)。指向派生类型的指针可隐式转换为基类指针。当不需要隐式转换时使用static_cast。此外,这种转换也是不可能的,因为转换在任何成员函数之外,因此无法访问私有基类。

此外,您不能将裸指针分配给唯一指针。要将裸指针的所有权转移到唯一指针,您可以使用unique_ptr::reset。但是,您永远不应该将来自 unique_ptr::get 的指针存储到另一个唯一指针中。当两个唯一指针析构函数都试图销毁同一个对象时,这样做会导致未定义的行为。幸运的是,在这种情况下,指针是值初始化的,因此为 null,因此该错误在技术上没有任何后果。但是您是否故意使用空指针?我怀疑不是。

将指向派生对象的唯一指针插入到指向基的唯一指针的映射中很简单。让ptr 成为指向Enemy 的唯一指针:

std::unique_ptr<Enemy> ptr = get_unique_pointer_from_somewhere();

只需移动分配唯一指针:

my_map[0] = std::move(ptr);

或者,您可以使用地图的emplace 成员函数。

最后,如果析构函数unique_ptr&lt;GameObject&gt; 指向派生对象,它将具有未定义的行为。要修复,请声明GameObject virtual 的析构函数。

【讨论】:

    猜你喜欢
    • 2017-11-04
    • 1970-01-01
    • 1970-01-01
    • 2016-08-20
    • 1970-01-01
    • 2018-07-11
    • 2016-06-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多