编写/设计一个简单的对象管理器(游戏上下文)

时间:2014-01-23 18:59:45

标签: c++ c++11

这个问题有很多部分,这就是标题有点模糊的原因。为简洁起见,省略了一些语法。大多数情况只是使用一些方便的学习例子,我的意思可能会变得明显。

我从这样的事情开始:

class EntityManager {
    // ...
  private:
    map<string, Entity> _entities;
}
// definition of some add method
void EntityManager::add ( ... ) {
    _entities.emplace( std::piecewise_construct,
                       std::forward_as_tuple( ... ),
                       /* and so on */ );
}

地图将保存整个游戏实体(大小固定),添加将涉及emplace

问题1。这很聪明吗?或者以某种方式效率低效而不是将指针作为地图中的值?

问题2。假设Entity(或某些子类)在某种程度上变得动态。有没有什么可以阻止这种方法处理它?<​​/ p>

我假设emplace将在堆上分配对象并维护内部指针。我可以像这样写一个get方法来对待它,就像参考一样。

Entity& EntityManager::get(std::string entityId)
{
    auto results = _gameEntities.find(entityId);
    if (results == _gameEntities.end()) {
        return Entity(); //this is bad, right?
    }

    return results->second;
}

问题3。当然,返回对本地堆栈分配对象的引用是不好的。由于我不能返回NULL,这是使用异常的唯一解决方案吗?

我注意到这种map<string, Entity>方法的另一个“问题”是,我只能通过先add来创建我正在创建的游戏对象,然后请求用get引用它,然后进行修改。这有点难看,也为对象创建增加了一些开销。

现在,我切换到指针设计:

class EntityManager {
    // ...
  private:
    map<string, unique_ptr<Entity>> _entities;
}

这给了我两个如何处理添加项目的选项

// OPTION 1: create a unique_ptr elsewhere, then move it.

void EntityManager::add( string id, unique_ptr<Entity> ptr) {
    _entities.insert( std::make_pair(id, std::move(ptr)) );
}

// object creation
unique_ptr<Entity> uPtr(new Entity());
_entityManager.add("entity1", std::move(uPtr));

// OR

// OPTION 2: Create a naked pointer elsewhere, create a unique_ptr on insert

void EntityManager::add( string id, Entity* ptr ) {
    _entities.insert(make_pair(id, unique_ptr<Entity>(ptr)));
}

// object creation
Entity* e = new Entity();
_entityManager.add("entity1", e);

问题4。这些方法之间是否存在任何差异?或者首选方式?

我认为这是我拥有的所有具体项目,感谢阅读。其实...

问题5。我错过了哪些错误/值得注意的错误?

1 个答案:

答案 0 :(得分:3)

问题1:我认为这种做法没有明显错误。从效率的角度来看,这对我来说似乎是合理的。

问题2:在C ++中,特定的类和类型总是固定大小的,所以我不明白你的问题。当你谈到一个动态大小的类型时,我可以想到两种解释:

  • 该类在内部分配和管理免费存储内存,动态增加对象的“实际”大小。 (这种情况也适用于具有这种“动态”数据成员的类,例如std::string。)在这种情况下,类型在结构上是固定大小的,虽然语义可变大小,但这都隐藏在后面类型的构造函数,析构函数和赋值运算符。因此,我认为它不会对您的设计产生重大影响。
  • 容器实际上用于保存异构类型,通过公共基类型相关。在这种情况下,您无法将对象直接存储在map(或其他标准容器)中,因为它们仅设计为包含同类型。您必须存储指针或指针类型(例如shared_ptr<Entity>unique_ptr<Entity>),并且您可能还需要特别考虑如何创建这些对象。在处理这些问题时有很多细节和权衡,所以我认为如果没有更多细节,我不能对这个问题给出具体的答案。

emplace()将在堆上分配对象,但(很可能)只是间接分配。更具体地说,您的对象将包装在内部节点结构中,这些结构本身是在堆上分配的。这些节点结构将包含您的对象以及指向其他节点的指针和/或由map维护的其他簿记信息。

问题3:如果您要查找的对象可能不存在,这是考虑使用指针的一个原因,因为它们可以为空并且可以针对nullptr进行测试。如果必须返回引用而不是指针,则异常是一个合理的选择(正如你所提到的那样隐式添加对象),但另一种选择是使用static原型对象并返回对它的引用:

Entity &EntityManager::get( string const &entityId) {
    static Entity empty{};
    auto found = gameEntities_.find(entityId);
    if (found == end(gameEntities_) )
        return empty;
    return found->second;
}
// (Note that leading underscores are reserved identifiers
// in some contexts and some such identifiers are reserved
// everywhere, so I prefer to avoid them altogether.)

然而,要意识到这种方法会使您的对象成为非线程安全的,因为相同的静态实体可能被多个彼此不了解的线程使用。如果get()返回Entity const &而不是Entity &,则此问题就会消失,因为只读变量本质上是线程安全的。另一种选择是使empty变量成为本地线程,但是如果empty值发生变化,则会产生一些成本并引入一些令人惊讶的行为 - 通过后续调用{{}可以看到更改1}}。

问题4:是的,这两种方法之间存在重大差异。具体来说, OPTION 2 不是例外安全的。您将get()按值传递给id,然后按值将其传递到add()。这是make_pair()复制构造函数抛出的两个机会,它将泄漏string

(另外,在 OPTION 1 中,您需要ptr,因为move(uPtr)可以移动,但不能复制。

问题5:我的评论包含在我对问题1-4的回答中。