C++ 11智能指针语义

Sam*_*lMS 37 c++ pointers smart-pointers c++11

我已经使用了几年的指针,但我最近才决定转换到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_;
    };
    
    Run Code Online (Sandbox Code Playgroud)

    然而,我对此的理解是,新的纹理将从传递的引用中复制构造,然后由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_;
    };
    
    Run Code Online (Sandbox Code Playgroud)

    与unique_ptr情况不同,我不必自己复制构造纹理,但渲染游戏对象很昂贵,因为我每次都必须锁定weak_ptr(复制构造一个新的shared_ptr很复杂).

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

我知道智能指针固有地比原始指针更复杂,因此我必须在某处损失,但这些成本似乎都高于应有的水平.

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

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

Rei*_*ica 31

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

如果该活得比-的指针保证是不是有效的,这将是有意义的纹理存储shared_ptr在经理和使用或者shared_ptrS或weak_ptrS IN的游戏对象,根据游戏对象的所有权语义与问候的纹理.你甚至可以反转 - shared_ptr在对象中存储s和weak_ptr在管理器中存储s .这样,管理器将作为缓存 - 如果请求纹理并且它weak_ptr仍然有效,它将给出它的副本.否则,它将加载纹理,给出一个shared_ptr并保持一个weak_ptr.

  • 当您需要*reseatable*非拥有的普通引用时,应该使用原始指针.否则,您应该使用引用.实际上,OP给出的示例似乎是一个简单引用的文本书用例.特别是因为他明确表示终生不是问题. (5认同)
  • @BjörnPollexCatatable...如果你想让你的对象可以分配(甚至只是可移动分配),你需要它.将引用作为成员变量是可能的,但它带来了一系列令人头疼的问题.我相信它最适用于特殊用例. (3认同)

Jon*_*alb 11

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

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

一种方法是让您的TextureManager返回纹理const*.考虑:

using TextureRef = Texture const*;
...
TextureRef TextureManager::texture(const std::string& key) const;
Run Code Online (Sandbox Code Playgroud)

因为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()};
Run Code Online (Sandbox Code Playgroud)

解决这个问题的一种方法是使用像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
};
Run Code Online (Sandbox Code Playgroud)

...

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()};
Run Code Online (Sandbox Code Playgroud)

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

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