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

Izz*_*zzo 2 c++ inheritance smart-pointers unique-ptr

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

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

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

但是,我实际上很难将衍生对象(例如Enemy和Loot)添加到此地图中.

我希望我的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;
}
Run Code Online (Sandbox Code Playgroud)

asc*_*ler 5

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

所以改进#1:

my_map[0] = dynamic_cast<GameObject*>(std::unique_ptr<Enemy>().get());
Run Code Online (Sandbox Code Playgroud)

但这不起作用,因为GameObject是私人基类Enemy.你可能想要使用公共继承,但是(当使用class而不是struct)时你必须这样说:

class Enemy : public GameObject
// ...
Run Code Online (Sandbox Code Playgroud)

接下来我们会发现=map语句中的内容无效.左侧有类型std::unique_ptr<GameObject>,没有任何operator=可以GameObject*指针的类型.但它确实有一个reset用于设置原始指针的成员:

my_map[0].reset(dynamic_cast<GameObject*>(std::unique_ptr<Enemy>().get()));
Run Code Online (Sandbox Code Playgroud)

现在声明应该编译 - 但它仍然是错误的.

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

my_map[0].reset(std::unique_ptr<Enemy>().get());
Run Code Online (Sandbox Code Playgroud)

下一个问题是std::unique_ptr<Enemy>()创建一个null unique_ptr,根本没有Enemy创建任何对象.要创建一个实际的Enemy,我们可以改为编写std::unique_ptr<Enemy>(new Enemy)或者std::make_unique<Enemy>().

my_map[0].reset(std::make_unique<Enemy>().get());
Run Code Online (Sandbox Code Playgroud)

仍然是错误的,并且有点棘手.现在问题是创建的Enemy对象由std::unique_ptr<Enemy>返回的临时对象拥有make_unique.该reset告诉std::unique_ptr<GameObject>它应该拥有一个指向同一个对象可在地图内.但是在声明的最后,临时std::unique_ptr<Enemy>被破坏,它会破坏Enemy对象.所以地图留下了一个指向死对象的指针,这个指针无效 - 不是你想要的.

但这里的解决方案是,我们不需要更动get(),并reset()在所有.有一个operator=允许std::unique_ptr<Enemy>为a 分配rvalue std::unique_ptr<GameObject>,它在这里做正确的事情.它利用隐式转换从Enemy*GameObject*.

my_map[0] = std::make_unique<Enemy>();
Run Code Online (Sandbox Code Playgroud)

(注意,如果你有一个名字 std::unique_ptr<Enemy>,你需要std::move它来允许分配,如同my_map[0] = std::move(enemy_ptr);.但std::move上面不需要,因为结果make_unique已经是一个右值.)

现在这个陈述更短,更清晰,实际上会做你想要的.

评论也提出了可能性

my_map.emplace(0, std::make_unique<Enemy>());
Run Code Online (Sandbox Code Playgroud)

这也是有效的,但是可能存在一个重要的区别:如果地图已经有一个键为零的对象,那么=版本将销毁并替换旧的emplace版本,但版本将单独保留地图,而刚刚创建的Enemy将被销毁.