C ++ 11智能指针语义

我已经使用了几年的指针,但是我最近才决定转换到C ++ 11的智能指针(即独特,共享和弱)。 我对它们做了一些相当的研究,这些是我绘制的结论:

  1. 独特的指针是伟大的。 他们pipe理自己的记忆,并像原始指针一样轻量级。 尽可能优先于原始指针的unique_ptr。
  2. 共享指针是复杂的。 由于引用计数,它们有很大的开销。 通过const引用传递它们,或者遗憾你的方法的错误。 他们不是邪恶的,但应该谨慎使用。
  3. 共享指针应该拥有自己的对象; 当不需要所有权时使用弱指针。 locking一个weak_ptr具有与shared_ptr拷贝构造函数相同的开销。
  4. 继续忽略auto_ptr的存在,现在已经废弃了。

所以,考虑到这些原则,我着手修改我的代码库,以利用我们新的shiny的智能指针,完全打算尽可能多地指明原始指针。 然而,我很困惑,关于如何最好地利用C ++ 11智能指针。

举个例子,假设我们正在devise一个简单的游戏。 我们决定将一个虚构的Texture数据types加载到一个TextureManager类中是最佳的。 这些纹理是复杂的,所以将它们按价值传递是不可行的。 此外,让我们假设游戏对象需要特定的纹理,取决于它们的对象types(即汽车,船等)。

在此之前,我会将纹理加载到一个向量(或者像unordered_map这样的其他容器)中,并在每个相应的游戏对象中存储指向这些纹理的指针,以便在需要渲染时可以引用它们。 我们假设纹理保证比他们的指针长。

那么我的问题就是如何在这种情况下最好地利用智能指针。 我看到几个选项:

  1. 将纹理直接存储在容器中,然后在每个游戏对象中构造一个unique_ptr。

    class TextureManager { public: const Texture& texture(const std::string& key) const { return textures_.at(key); } private: std::unordered_map<std::string, Texture> textures_; }; class GameObject { public: void set_texture(const Texture& texture) { texture_ = std::unique_ptr<Texture>(new Texture(texture)); } private: std::unique_ptr<Texture> texture_; }; 

    然而,我的理解是,一个新的纹理将从被传递的引用被复制构build,然后被unique_ptr所拥有。 这使我感到非常不受欢迎,因为我会拥有与使用它的游戏对象一样多的纹理副本 – 破坏指针点(不是双关语)。

  2. 不直接存储纹理,而是将它们的共享指针存储在容器中。 使用make_shared来初始化共享指针。 构build游戏对象中的弱指针。

     class TextureManager { public: const std::shared_ptr<Texture>& texture(const std::string& key) const { return textures_.at(key); } private: std::unordered_map<std::string, std::shared_ptr<Texture>> textures_; }; class GameObject { public: void set_texture(const std::shared_ptr<Texture>& texture) { texture_ = texture; } private: std::weak_ptr<Texture> texture_; }; 

    与unique_ptr不同的是,我不需要自己复制构造纹理,但是渲染游戏对象的代价很​​高,因为每次都必须lockingweak_ptr(与复制构build新的shared_ptr一样复杂)。

所以总结一下,我的理解是这样的:如果我要使用唯一的指针,我将不得不复制构造纹理; 或者,如果我要使用共享和弱指针,那么每次绘制游戏对象时都必须复制构build共享指针。

我明白,智能指针本质上会比原始指针更复杂,所以我必然会在某个地方亏本,但是这两项成本似乎都比他们应该高。

有人能指出我的方向吗?

对不起长时间的阅读,感谢您的时间!

即使在C ++ 11中,原始指针仍然作为对对象的非拥有引用完全有效 。 就你而言,你的意思是“让我们假设纹理保证能够超越他们的指针”。 这意味着你可以非常安全地使用原始指针指向游戏对象中的纹理。 在纹理pipe理器中,将纹理自动存储(保存在内存中的恒定位置的容器中),或存储在unique_ptr的容器中。

如果指针保证无效,将纹理存储在pipe理器的shared_ptr中是合理的,并根据游戏对象的所有权语义在游戏对象中使用shared_ptr或者weak_ptr 。纹理。 你甚至可以反转 – 将shared_ptr存储在pipe理器中的objects和weak_ptr中。 这样,pipe理器就可以作为一个caching – 如果一个纹理被请求并且它的weak_ptr仍然有效,它将会给出一个副本。 否则,它会加载纹理,发出一个shared_ptr并保持一个weak_ptr

总结一下你的用例:*)对象保证他们的用户活跃*)对象,一旦创build,不会被修改(我认为这是暗示你的代码)*)对象是可引用的名称,并保证存在任何你的应用程序将要求的名称(我推断 – 如果这不是真的,我会在下面处理该怎么做。)

这是一个令人愉快的用例。 您可以在整个应用程序中使用纹理的值语义! 这具有很好的性能和易于推理的优点。

一种方法是让TextureManager返回一个Texture const *。 考虑:

 using TextureRef = Texture const*; ... TextureRef TextureManager::texture(const std::string& key) const; 

由于下层的Texture对象具有应用程序的生命周期,永远不会被修改,并且始终存在(您的指针永远不会是nullptr),因此您可以将TextureRef作为简单值处理。 你可以传递它们,返回它们,比较它们,并制作它们的容器。 他们很容易推理,而且工作效率很高。

这里的烦恼是你有价值的语义(这是很好),但指针语法(这可能会混淆types与价值语义)。 换句话说,要访问你的Texture类的成员,你需要做这样的事情:

 TextureRef t{texture_manager.texture("grass")}; // You can treat t as a value. You can pass it, return it, compare it, // or put it in a container. // But you use it like a pointer. double aspect_ratio{t->get_aspect_ratio()}; 

解决这个问题的方法之一就是使用像pimpl习语这样的东西,并创build一个只不过是指向纹理实现的指针的包装类。 这是一个更多的工作,因为你最终将创build一个API(成员函数)你的纹理包装类转发到你的实现类的API。 但好处是你有一个纹理类,同时具有值​​语义和值语法。

 struct Texture { Texture(std::string const& texture_name): pimpl_{texture_manager.texture(texture_name)} { // Either assert(pimpl_); // or if (not pimpl_) {throw /*an appropriate exception */;} // or do nothing if TextureManager::texture() throws when name not found. } ... double get_aspect_ratio() const {return pimpl_->get_aspect_ratio();} ... private: TextureImpl const* pimpl_; // invariant: != nullptr }; 

 Texture t{"grass"}; // t has both value semantics and value syntax. // Treat it just like int (if int had member functions) // or like std::string (except lighter weight for copying). double aspect_ratio{t.get_aspect_ratio()}; 

我假定在你的游戏环境中,你永远不会要求一个不能保证存在的纹理。 如果是这样的话,那么你可以断言名称存在。 但如果情况并非如此,那么你需要决定如何处理这种情况。 我的build议是将它作为你的包装类的不变,指针不能是nullptr。 这意味着如果纹理不存在,则从构造函数中抛出。 这意味着当您尝试创buildTexture时,您会处理这个问题,而不是每次调用包装类的成员时都必须检查一个空指针。

在回答你原来的问题时,智能指针对于生命周期pipe理是非常有价值的,如果你只需要传递一个对象的指针,而这个对象的生存期将会保持在指针之外,那么它就不是特别有用。

你可以有一个std :: unique_ptrs的std :: map来存储纹理。 然后,您可以编写一个get方法,该方法返回按名称引用的纹理。 这样,如果每个模型知道它的纹理的名称(它应该),你可以简单地将名称传入get方法,并从地图中检索引用。

 class TextureManager { public: Texture& get_texture(const std::string& key) const { return *textures_.at(key); } private: std::unordered_map<std::string, std::unique_ptr<Texture>> textures_; }; 

那么你可以在游戏对象类中使用一个Texture,而不是Texture *,weak_ptr等。

这种方式纹理pipe理器可以像一个caching,get方法可以被重写,以search纹理,如果发现从地图返回它,否则加载它,将其移动到地图,然后返回一个引用

在我走之前,因为我不小心写了一本小说

TL; DR使用共同的指针来计算责任问题,但要谨慎周期性的关系。 如果我是你,我会使用一个共享指针表来存储你的资产,所有需要这些共享指针的指针也应该使用一个共享指针。 这消除了读取弱指针的开销(因为游戏的开销就像每个对象每秒创build一个新的智能指针60次)。 这也是我的团队和我所采取的方法,这是非常有效的。 你也可以说你的纹理保证活跃,所以你的对象不能删除纹理,如果他们使用共享指针。

如果我能够投入2分钱,我想告诉你几乎和我自己的video游戏中的智能指针一样的尝试。 无论好坏。

这个游戏的代码在解决scheme#2中采用了几乎完全相同的方法:一个表格中填充了位图的智能指针。

我们虽然有一些分歧, 我们决定将我们的位图表分成两部分:一个用于“紧急”位图,另一个用于“轻松”位图。 紧急的位图是不断加载到内存中的位图,并将在战斗中使用,我们现在需要animation,而不想去硬盘,这个硬盘有一个非常明显的口吃。 简易表是一个由硬盘上的位图组成的文件path表。 这些将会是在相当长的一段游戏开始时加载的大的位图; 像你的angular色的行走animation,或背景图像。

在这里使用原始指针有一些问题,特别是所有权。 看,我们的资产表有一个Bitmap *find_image(string image_name)函数。 这个函数将首先在紧急表中search与image_name匹配的条目。 如果find,太棒了! 返回一个位图指针。 如果找不到,则search简易表。 如果我们find一个匹配你的图像名称的path,创build位图,然后返回该指针。

最使用这个类的是我们的animation类。 这是所有权问题:何时animation应该删除它的位图? 如果它来自简单的桌子,那么没有问题; 该位图是专门为您创build的。 删除它是你的责任!

但是,如果你的位图来自紧急表,你不能删除它,因为这样做会阻止其他人使用它,你的程序就像ET游戏一样失败,你的销售就跟着走了。

没有智能指针,这里唯一的解决scheme是让Animation类无论如何克隆它的位图。 这允许安全的删除,但是杀死程序的速度。 这些图像是不是应该是时间敏感的?

但是,如果资产类返回一个shared_ptr<Bitmap> ,那么您无需担心。 我们的资产表是静态的,所以无论如何这些指针都会持续到程序结束。 我们把函数改成了shared_ptr<Bitmap> find_image (string image_name) ,再也不用克隆一下bitmap了。 如果位图来自简易表,那么该智能指针是唯一的一种,并且被animation删除。 如果是一个紧急的位图,那么该表仍然保留了animation销毁的参考,并且数据被保存。

这是最高兴的部分,这是丑陋的一部分。

我发现共享和独特的指针是伟大的,但他们肯定有他们的警告。 对于我来说最大的一个并没有明确的控制你的数据被删除的时间。 共享指针保存了我们的资产查找,但在执行时却杀死了游戏的其余部分。

看,我们有一个内存泄漏,并认为“我们应该到处使用智能指针!”。 巨大的错误。

我们的游戏有一个由Environment控制的GameObjects 。 每个环境都有一个GameObject *的向量,每个对象都有一个指向其环境的指针。

你应该看看我要去哪里。

物体有方法将自己从环境中“推”出来。 这将是万一他们需要移动到一个新的地区,或者可能传送,或通过其他物体的阶段。

如果环境是对象的唯一引用持有者,那么你的对象不会被删除而离开环境。 这通常发生在制造射弹时,尤其是传送射弹。

对象也正在删除他们的环境,至less如果他们是最后一个离开它。 大多数游戏状态的环境也是一个具体的对象。 我们打电话删除堆栈! 是的,我们是业余的,起诉我们。

根据我的经验,当你懒得调用delete时只使用unique_pointers,只有一件东西会拥有你的对象,当你想让多个对象指向一件事物,但不能决定谁删除时,使用shared_pointers,对shared_pointers的周期性关系非常谨慎。