37

几年来我一直在使用指针,但直到最近我才决定过渡到 C++11 的智能指针(即唯一、共享和弱指针)。我已经对它们进行了相当多的研究,这些是我得出的结论:

  1. 独特的指针很棒。它们管理自己的内存,并且像原始指针一样轻量级。尽可能选择 unique_ptr 而不是原始指针。
  2. 共享指针很复杂。由于引用计数,它们有很大的开销。通过 const 引用传递它们或对你的方式错误感到遗憾。它们不是邪恶的,但应该谨慎使用。
  3. 共享指针应该拥有对象;在不需要所有权时使用弱指针。锁定一个weak_ptr 与shared_ptr 复制构造函数的开销相当。
  4. 继续忽略 auto_ptr 的存在,无论如何现在它已被弃用。

因此,考虑到这些原则,我着手修改我的代码库以利用我们新的闪亮智能指针,完全打算清除尽可能多的原始指针。然而,我对如何最好地利用 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 一样复杂)。

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

我知道智能指针本质上会比原始指针更复杂,所以我一定会在某个地方蒙受损失,但这两种成本似乎都比它们应该的要高。

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

抱歉,阅读时间长,感谢您的宝贵时间!

4

4 回答 4

31

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

如果指针寿命保证无效shared_ptr,则将纹理存储在管理器中并在游戏对象中使用shared_ptrs 或s 是有意义的weak_ptr,具体取决于游戏对象关于纹理的所有权语义。您甚至可以反转它 - 将shared_ptrs 存储在对象中,将 s 存储weak_ptr在管理器中。这样,管理器将充当缓存 - 如果请求纹理并且它weak_ptr仍然有效,它将给出它的副本。否则,它将加载纹理,发出 ashared_ptr并保留 a weak_ptr

于 2013-11-12T13:11:46.757 回答
11

总结一下您的用例: *) 对象保证比其用户寿命长 *) 对象一旦创建就不会被修改(我认为这是您的代码所暗示的)*) 对象可以按名称引用,并且保证对任何对象都存在您的应用程序将要求的名称(我在推断 - 如果这不是真的,我将在下面处理。)

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

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

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

因为底层 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 idiom 并创建一个类,该类只不过是指向纹理实现的指针的包装器。这是一个多一点的工作,因为您最终将为您的纹理包装类创建一个 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。这意味着如果纹理不存在,则从构造函数中抛出。这意味着您在尝试创建纹理时处理问题,而不是每次调用包装器类的成员时都必须检查空指针。

在回答您最初的问题时,智能指针对生命周期管理很有价值,并且如果您只需要传递对生命周期保证比指针更长的对象的引用,则智能指针并不是特别有用。

于 2013-11-13T23:41:18.110 回答
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 方法来搜索纹理,如果找到则从地图中返回它,否则首先加载它,将其移动到地图然后返回一个 ref 给它

于 2013-11-12T13:16:00.050 回答
1

在我开始之前,因为我不小心一本小说......

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

如果我能投入 2 美分,我想告诉你我在自己的电子游戏中使用智能指针进行的几乎相同的尝试;好的和坏的。

该游戏的代码采用与您的解决方案#2 几乎相同的方法:一个充满位图智能指针的表格。

不过我们有一些不同;我们决定将位图表分成两部分:一张用于“紧急”位图,另一张用于“方便”位图。紧急位图是不断加载到内存中的位图,将在战斗中使用,我们现在需要动画并且不想进入硬盘,它有一个非常明显的卡顿。简易表是硬盘上位图的文件路径字符串表。这些将是在相对较长的游戏部分开始时加载的大型位图;比如你角色的行走动画,或者背景图片。

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

使用最多的类肯定是​​我们的动画类。这里是所有权问题:动画什么时候应该删除它的位图?如果它来自简易桌子,那么没有问题;该位图是专门为您创建的。删除它是你的责任!

但是,如果您的位图来自紧急表,则不能将其删除,因为这样做会阻止其他人使用它,并且您的程序会像 ET 游戏一样崩溃,您的销售也会随之而来。

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

但是,如果资产类返回 a shared_ptr<Bitmap>,那么您无需担心。你看,我们的资产表是静态的,所以不管怎样,这些指针一直持续到程序结束。我们将函数更改为shared_ptr<Bitmap> find_image (string image_name),并且不再需要克隆位图。如果位图来自简易表,则该智能指针是同类中唯一的一个,并与动画一起被删除。如果是紧急位图,则在 Animation 销毁时,该表仍保留引用,并保留数据。

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

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

看,我们有内存泄漏,并认为“我们应该在任何地方使用智能指针!”。巨大的错误。

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

你应该看看我要去哪里。

对象有方法可以将自己从环境中“弹出”。这是为了以防他们需要移动到一个新的区域,或者可能是传送,或者穿过其他物体。

如果环境是对象的唯一引用持有者,那么您的对象无法在不被删除的情况下离开环境。这通常在创建射弹时发生,尤其是传送射弹。

对象也正在删除它们的环境,至少如果它们是最后一个离开它的话。大多数游戏状态的环境也是一个具体的对象。我们在堆栈上调用删除!是的,我们是业余爱好者,起诉我们。

根据我的经验,当您懒得调用 delete 并且只有一件事将拥有您的对象时,请使用 unique_pointers,当您希望多个对象指向一件事但无法决定谁必须删除它时,请使用 shared_pointers,并且非常警惕与 shared_pointers 的周期性关系。

于 2015-08-07T00:30:40.150 回答