关于c ++:C ++ 11智能指针语义

C++11 Smart Pointer Semantics

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

  • 独特的指针非常棒。它们管理自己的内存,和原始指针一样轻。与原始指针相比,尽可能使用独特的指针。
  • 共享指针很复杂。由于引用计数,它们有很大的开销。通过常量引用传递它们,或者后悔你的方法的错误。它们不是邪恶的,但应该谨慎使用。
  • 共享指针应该拥有对象;在不需要所有权时使用弱指针。锁定弱指针的开销与共享指针复制构造函数的开销相等。
  • 继续忽略auto-ptr的存在,不管怎样,它现在已经被否决了。
  • 因此,考虑到这些原则,我开始修改我的代码库,以利用我们新的闪亮的智能指针,完全打算向Board清除尽可能多的原始指针。然而,对于如何最好地利用C++ 11智能指针,我感到困惑。

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

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

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

  • 将纹理直接存储在一个容器中,然后在每个游戏对象中构建一个唯一的指针。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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_;
    };

    然而,我的理解是,一个新的纹理将由传递的引用复制,然后由UnQuyJPTR拥有。这对我来说是非常不受欢迎的,因为我将有许多副本的纹理作为游戏对象使用它-击败点的指针(没有双关打算)。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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_;
    };

    与独特的_-ptr情况不同,我不需要复制构造纹理本身,但是渲染游戏对象是昂贵的,因为我每次都必须锁定弱_-ptr(与复制构造新的共享_-ptr一样复杂)。

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

    我知道聪明的指针天生比原始指针更复杂,所以我必须在某个地方亏损,但这两个成本似乎都比它们应该的要高。

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

    很抱歉读了这么久,谢谢你的时间!


    即使在C++ 11中,原始指针仍然是完全有效的,而不是拥有对象的引用。在你的例子中,你是说"让我们假设纹理保证比它们的指针寿命长。"这意味着你完全可以安全地使用原始指针指向游戏对象中的纹理。在纹理管理器中,自动(在保证内存中位置不变的容器中)或在unique_ptr的容器中存储纹理。

    如果离群指针保证无效,则将纹理存储在管理器中的shared_ptr中,并在游戏对象中使用shared_ptrs或weak_ptrs,这是有意义的,这取决于游戏对象相对于纹理的所有权语义。您甚至可以将该-store shared_ptr存储在对象中,weak_ptr存储在管理器中。这样,管理器将充当缓存——如果请求纹理,并且其weak_ptr仍然有效,它将发出一个纹理副本。否则,它将加载纹理,发出一个shared_ptr并保留一个weak_ptr


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

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

    一种方法是让你的纹理管理器返回一个纹理常量*。考虑:

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

    因为Undering纹理对象具有应用程序的生命周期,从不被修改,并且始终存在(指针从不为nullptr),所以您可以将TextureRef视为简单值。你可以传递它们,返回它们,比较它们,并用它们做容器。他们很容易推理,而且工作效率很高。

    这里的麻烦之处在于,您有值语义(这很好),但是指针语法(对于具有值语义的类型,这可能会混淆)。换句话说,要访问纹理类的成员,需要执行如下操作:

    1
    2
    3
    4
    5
    6
    7
    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将转发到实现类的API。但是优点是您有一个同时具有值语义和值语法的纹理类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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
    };

    1
    2
    3
    4
    5
    6
    7
    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。这意味着,如果纹理不存在,则从构造函数抛出。这意味着您在尝试创建纹理时要处理该问题,而不是每次调用包装类的成员时都要检查空指针。

    在回答您最初的问题时,智能指针对于生命周期管理很有价值,如果您只需要将引用传递给保证生命周期超过指针的对象,那么智能指针就不特别有用了。


    你可以有一个STD::STD图::UnQuyjpTRS,其中纹理被存储。然后,可以编写一个get方法,该方法通过名称返回对纹理的引用。这样,如果每个模型都知道其纹理的名称(它应该知道),您可以简单地将该名称传递到get方法中,并从映射中检索引用。

    1
    2
    3
    4
    5
    6
    7
    8
    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_;
    };

    然后您可以在游戏对象类中使用纹理,而不是纹理*、弱指针等。

    这样,纹理管理器可以像缓存一样工作,可以重新编写get方法来搜索纹理,如果找到,则将其从映射中返回,否则首先加载,将其移动到映射中,然后返回对其的引用。


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

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

    如果我能投入2美分,我想告诉你一个几乎完全相同的尝试,我在我自己的电子游戏智能指针,无论好坏。好的。

    这个游戏的代码对您的解决方案采用了几乎相同的方法2:一个充满指向位图的智能指针的表。好的。

    不过,我们有一些不同;我们决定将位图表分成两部分:一部分用于"紧急"位图,另一部分用于"方便"位图。紧急位图是不断载入内存的位图,将在战斗中使用,在战斗中,我们现在需要动画,不想去硬盘,硬盘有一个非常明显的结巴。Facile表是指向HDD上位图的文件路径字符串表。这些将是在相对较长的游戏;比如你角色的行走动画,或者背景图像。好的。

    使用原始指针有一些问题,特别是所有权。请参阅,我们的资产表具有EDCOX1×0函数。这个函数将首先搜索紧急表中的匹配EDOCX1×1的条目。如果找到了,太好了!返回一个位图指针。如果找不到,请搜索Facile表。如果我们找到与您的图像名匹配的路径,请创建位图,然后返回该指针。好的。

    最常用的课程肯定是我们的动画课。这里有一个所有权问题:动画什么时候应该删除它的位图?如果它来自Facile表,那么没有问题;该位图是专门为您创建的。删除它是你的责任!好的。

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

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

    但是,如果资产类要返回一个shared_ptr,那么就没有什么可担心的了。我们的资产表是静态的,所以这些指针一直持续到程序结束,不管发生什么。我们将函数改为shared_ptr find_image (string image_name),不再需要克隆位图。如果位图来自Facile表,那么智能指针是唯一的一种,并随动画一起删除。如果这是一个紧急位图,那么在动画销毁时,表仍然保留一个引用,并保留数据。好的。

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

    我发现共享的和独特的指针是伟大的,但他们肯定有他们的警告。对我来说,最大的一个问题是无法明确控制何时删除数据。共享指针保存了我们的资产查找,但在实现上扼杀了游戏的其余部分。好的。

    看,我们有一个内存泄漏,并且认为"我们应该在任何地方使用智能指针!"大错特错。好的。

    我们的游戏有EDOCX1×2,这是由EDCX1〔3〕控制的。每个环境都有一个EDOCX1×4的向量,每个对象都有一个指向其环境的指针。好的。

    你应该看看我要去哪里。好的。

    对象有从其环境中"弹出"自己的方法。这是为了防止他们需要移动到一个新的区域,或者传送,或者通过其他物体的相位。好的。

    如果环境是对象的唯一引用持有者,那么您的对象就不能离开环境而不被删除。这种情况通常发生在创建投射物时,尤其是传送投射物时。好的。

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

    在我的经验中,当你懒得调用delete并且只有一件事会拥有你的对象时,使用唯一的指针;当你想让多个对象指向一件事情时,使用共享指针,但是不能决定谁必须删除它,并且要非常小心与共享指针的循环关系。好的。好啊。