C ++ 11智能指针语义

时间:2013-11-12 13:03:29

标签: c++ pointers c++11 smart-pointers

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

  1. 独特的指针很棒。他们管理自己的内存,并且像原始指针一样轻量级。首选尽可能多地使用unique_ptr而不是原始指针。
  2. 共享指针很复杂。由于引用计数,它们具有显着的开销。通过const引用传递它们或后悔你的方式的错误。他们不是邪恶的,但应该谨慎使用。
  3. 共享指针应该拥有对象;在不需要所有权时使用弱指针。锁定weak_ptr会产生与shared_ptr复制构造函数相同的开销。
  4. 继续忽略auto_ptr的存在,无论如何现在已弃用。
  5. 因此,考虑到这些原则,我开始修改我的代码库以利用我们新的闪亮智能指针,完全打算清除尽可能多的原始指针。然而,我对如何最好地利用C ++ 11智能指针感到困惑。

    例如,我们假设我们正在设计一个简单的游戏。我们认为将虚构的Texture数据类型加载到TextureManager类中是最佳的。这些纹理很复杂,因此按值传递它们是不可行的。此外,让我们假设游戏对象根据其对象类型(即汽车,船等)需要特定的纹理。

    之前,我会将纹理加载到矢量(或其他容器,如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_;
      };
      

      然而,我对此的理解是,新的纹理将从传递的引用中复制构造,然后由unique_ptr拥有。这让我觉得非常不受欢迎,因为我会使用与使用它的游戏对象一样多的纹理副本 - 击败指针点(没有双关语)。

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

      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情况不同,我不必自己复制构造纹理,但渲染游戏对象很昂贵,因为我每次都必须锁定weak_ptr(复制构造一个新的shared_ptr很复杂)。

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

      我知道智能指针本质上会比原始指针更复杂,因此我必须在某个地方承担损失,但这两种成本似乎都高于应有的水平。

      有人能指出我正确的方向吗?

      很抱歉长时间阅读,谢谢你的时间!

4 个答案:

答案 0 :(得分:31)

即使在C ++ 11中,原始指针仍然完全有效作为对象的非拥有引用。在你的情况下,你说“让我们假设纹理保证比他们的指针更长。”这意味着您可以非常安全地使用游戏对象中纹理的原始指针。在纹理管理器中,自动存储纹理(在保证在内存中保持不变位置的容器中),或者在unique_ptr s的容器中存储纹理。

如果超出指针保证有效,那么将shared_ptr中的纹理存储在管理器中并使用{游戏对象中的{1}}或shared_ptr s,取决于游戏对象与纹理相关的所有权语义。你甚至可以反过来 - 在对象中存储weak_ptr,在管理器中存储shared_ptr。这样,管理器将作为缓存 - 如果请求纹理并且其weak_ptr仍然有效,它将给出它的副本。否则,它将加载纹理,发出weak_ptr并保留shared_ptr

答案 1 :(得分:11)

总结您的用例: *)保证对象比他们的用户寿命更长 *)对象一旦创建,就不会被修改(我认为这是你的代码隐含的) *)对象可以通过名称引用,并保证存在您的应用程序要求的任何名称(我正在推断 - 如果不是这样,我将在下面处理该怎么做。)

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

执行此操作的一种方法是让TextureManager返回纹理const *。考虑:

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

因为underling Texture对象具有应用程序的生命周期,永远不会被修改,并且始终存在(您的指针永远不会是nullptr),您可以将TextureRef视为简单值。您可以传递它们,返回它们,比较它们,并制作它们的容器。它们非常容易推理并且非常有效地工作。

这里的烦恼是你有价值语义(这很好),但是指针语法(对于具有值语义的类型可能会造成混淆)。换句话说,要访问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习惯用法这样的东西,并创建一个只是指向纹理实现的指针的包装的类。这是一项更多的工作,因为您最终将为纹理包装类创建一个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()};

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

在回答您的原始问题时,智能指针对于生命周期管理很有价值,如果您只需要传递对其生命周期保证超过指针的对象的引用,则它们并不是特别有用。

答案 2 :(得分:3)

你可以在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等。

这样纹理管理器可以像缓存一样,可以重写get方法来搜索纹理,如果找到则从地图返回它,否则首先加载它,将其移动到地图然后返回一个引用它呢

答案 3 :(得分:1)

在我开始之前,我不小心写了一部小说......

TL; DR使用共享指针来确定责任问题,但对周期性关系要非常谨慎。如果我是你,我会使用共享指针表来存储你的资产,而那些需要这些共享指针的东西也应该使用共享指针。这消除了用于读取的弱指针的开销(因为游戏中的开销就像每个对象创建一个新的智能指针60秒一样)。这也是我和我的团队采取的方法,而且非常有效。您还说保证纹理比对象寿命更长,因此如果对象使用共享指针,则无法删除纹理。

如果我可以投入2美分,我想告诉你我在自己的视频游戏中用智能指针进行的几乎相同的尝试;无论好坏。

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

但我们有一些差异;我们决定将我们的位图表分成2个部分:一个用于&#34;紧急&#34;位图,一个用于&#34; easyile&#34;位图。紧急位图是不断加载到内存中的位图,将在战斗中使用,我们现在需要动画,并且不想去硬盘,这有一个非常明显的口吃。简易表是一个表到硬盘上位图的文件路径的表。这些将是在相对较长的部分开始时加载的大位图 游戏;喜欢角色的步行动画或背景图像。

在这里使用原始指针有一些问题,特别是所有权。请参阅,我们的资产表具有Bitmap *find_image(string image_name)功能。此函数首先在紧急表中搜索匹配image_name的条目。如果找到了,太棒了!返回一个位图指针。如果未找到,请搜索easyile表。如果我们找到与您的图像名称匹配的路径,请创建位图,然后返回该指针。

使用它的类绝对是我们的Animation类。这是所有权问题:动画何时应删除其位图?如果它来自轻便的桌子,那么没有问题;该位图是专门为您创建的。你有责任删除它!

但是,如果您的位图来自紧急表,则无法将其删除,因为这样做会阻止其他人使用它,并且您的程序会像E.T.游戏,你的销售也跟风。

如果没有智能指针,这里唯一的解决方案就是让Animation类克隆其位图,无论如何。这允许安全删除,但会降低程序的速度。 Weren这些图像应该是时间敏感的吗?

但是,如果资产类要返回shared_ptr<Bitmap>,那么您无需担心。我们的资产表是你看到的静态,所以这些指针一直持续到程序结束,无论如何。我们将函数更改为shared_ptr<Bitmap> find_image (string image_name),并且永远不必再次克隆位图。如果位图来自facile表,那么该智能指针是同类中的唯一一个,并且随动画一起被删除。如果它是一个紧急位图,那么该表仍然保留了动画销毁的参考,并保留了数据。

这是快乐的部分,这是丑陋的部分。

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

看,我们有内存泄漏,并且认为&#34;我们应该到处使用智能指针!&#34;。巨大的错误。

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

你应该看到我的目标。

对象有方法&#34;弹出&#34;他们自己的环境。这将是他们需要移动到一个新的区域,或者传送或相位穿过其他对象。

如果环境是对象的唯一引用持有者,则您的对象无法离开环境而不会被删除。这通常发生在制造射弹时,特别是传送射弹。

对象也在删除他们的环境,至少如果他们是最后一个离开它的人。大多数游戏州的环境也是一个具体的对象。我们打算在堆栈上删除!是的,我们是业余爱好者,起诉我们。

根据我的经验,当你懒得调用删除时使用unique_pointers,只有一件东西会拥有你的对象,当你想要多个对象指向一件东西时使用shared_pointers,但不能决定谁必须删除它,并且非常警惕与shared_pointers的周期性关系。