如何将抽象基类unique_ptrs添加到地图?

时间:2018-06-28 03:04:29

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

我目前正在用C ++编写游戏。该游戏设有GameManager类。 GameManager类包含一个地图,其中包含指向游戏对象的指针。我定义了一个GameObject类,它是一个仅充当接口的抽象类。

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

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

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

我希望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;
}

2 个答案:

答案 0 :(得分:5)

产生错误消息的第一个原因是,类类型永远不能用作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<GameObject>,没有任何可以使用operator=指针的GameObject*。但是它确实有一个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<Enemy>()创建一个空unique_ptr,并且根本没有创建Enemy对象。要创建实际的Enemy,我们可以改写std::unique_ptr<Enemy>(new Enemy)std::make_unique<Enemy>()

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

还是错了,而且有点棘手。现在的问题是,创建的Enemy对象归std::unique_ptr<Enemy>返回的临时make_unique对象所有。 reset告诉映射中的std::unique_ptr<GameObject>应该拥有一个指向同一对象的指针。但是在该语句的末尾,临时std::unique_ptr<Enemy>被销毁,并且销毁了Enemy对象。因此,地图上留有指向死对象的指针,该对象无法使用-不是您想要的。

但是这里的解决方案是我们完全不需要弄乱get()reset()。有一个operator=可以为std::unique_ptr<Enemy>分配右值std::unique_ptr<GameObject>,并且在这里做正确的事情。它利用了从Enemy*GameObject*的隐式转换。

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

(请注意,如果您有一个名为 std::unique_ptr<Enemy>,则需要std::move才能进行分配,就像my_map[0] = std::move(enemy_ptr);一样。但是{{1上面不需要}},因为std::move的结果已经是右值。)

现在,此语句更短,更清晰,并且将实际执行您想要的操作。

评论还暗示了可能性

make_unique

这也是有效的,但可能存在重要的区别:如果地图已经有一个键为零的对象,则my_map.emplace(0, std::make_unique<Enemy>()); 版本将销毁并替换旧的键,而=版本将销毁旧的键离开地图,刚创建的emplace将被销毁。

答案 1 :(得分:0)

dynamic_cast仅可用于在指针和引用之间进行转换。 GameObject既不是指针类型,也不是引用类型,因此无法dynamic_cast对其进行访问。

您可能打算使用dynamic_cast<GameObject*>。但是,您不应该dynamic_cast指向基类(指向它)。指向派生类型的指针可以隐式转换为基类指针。当不需要隐式转换时,请使用static_cast。此外,该转换也不可能,因为强制转换在任何成员函数的外部,因此无法访问私有基类。

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

将派生对象的唯一指针插入到base的唯一指针的映射中很简单。假设ptr是指向Enemy的唯一指针:

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

只需移动即可分配唯一的指针:

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

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

最后,析构函数unique_ptr<GameObject>指向派生对象时将具有未定义的行为。要解决此问题,请声明GameObject虚拟的析构函数。