这个问题有很多部分,这就是标题有点模糊的原因。为简洁起见,省略了一些语法。大多数情况只是使用一些方便的学习例子,我的意思可能会变得明显。
我从这样的事情开始:
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。我错过了哪些错误/值得注意的错误?
答案 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的回答中。