• C++ 核心指南之资源管理(下)智能指针最佳实践


    C++ 核心指南(C++ Core Guidelines)是由 Bjarne Stroustrup、Herb Sutter 等顶尖 C+ 专家创建的一份 C++ 指南、规则及最佳实践。旨在帮助大家正确、高效地使用“现代 C++”。

    这份指南侧重于接口、资源管理、内存管理、并发等 High-level 主题。遵循这些规则可以最大程度地保证静态类型安全,避免资源泄露及常见的错误,使得程序运行得更快、更好。

    R.smart:智能指针

    • R.20:使用 unique_ptr 或 shared_ptr 来表示所有权
    • R.21:除非需要共享所有权,否则优先使用 unique_ptr 而不是 shared_ptr
    • R.22:使用 make_shared() 创建 shared_ptr
    • R.23:使用 make_unique() 创建 unique_ptr
    • R.24:使用 std::weak_ptr 打破 shared_ptr 的循环引用
    • R.30: 仅在需要明确表示生命周期语义时才将智能指针作为参数传递
    • R.31: 如果你使用的是非 std 智能指针,遵循 std 的基本模式
    • R.32: 将形参声明为 unique_ptr 来表明函数对 widget 的所有权
    • R.33: 将形参声明为 unique_ptr& 来表明函数对 widget 的重新赋值语义
    • R.34: 将形参声明为 shared_ptr 来明确表明函数共享所有权
    • R.35: 将形参声明为 shared_ptr& 来表明函数可能重新赋值共享指针
    • R.36: 将形参声明为 const shared_ptr& 来表明它可能增加对象的引用计数
    • R.37: 不要传递从“智能指针别名”取得的指针或引用

    R.20:使用 unique_ptr 或 shared_ptr 来表示所有权

    可以避免资源泄露

    void f()
    {
        // bad, p1 可能泄露
        X* p1 { new X };
        // good, 独占所有权
        auto p2 = make_unique();
        // good, 共享所有权
        auto p3 = make_shared();
    }
    

    代码检查

    • 如果 new 的结果赋给了裸指针,给出警告
    • 如果函数返回的“拥有指针”赋给了裸指针,给出警告

    R.21:除非需要共享所有权,否则优先使用 unique_ptr 而不是 shared_ptr

    unique_ptr 在概念上更简单、可预测(知道何时发生析构),而且更快(无需隐式维护使用计数)。

    反面例子

    增加和维护了一个不必要的引用计数

    void f()
    {
        shared_ptr base = make_shared();
        // 在本地使用 base,不需要拷贝,引用计数永远不会超过 1
    } // 销毁 base
    

    例子

    void f()
    {
        unique_ptr base = make_unique();
        // 在本地使用 base
    } // 销毁 base
    

    代码检查

    如果一个函数使用了一个在函数内部分配的 shared_ptr,但从不返回该 shared_ptr 或将其传递给形参为 shared_ptr& 的函数,则给出警告。建议使用 unique_ptr 替代。

    R.22:使用 make_shared() 创建 shared_ptr

    make_shared 提供了更简洁的构造语句。而且 make_shared 有机会将引用计数存储在其关联对象的相邻位置。

    示例

    shared_ptr p1 { new X{2} }; // BAD
    auto p = make_shared(2); // GOOD
    

    使用 make_shared() 版本只提到了 X 一次,所以它通常比使用显式 new 的版本更短,同时也更快。

    代码检查

    如果一个 shared_ptr 是由 new 的结果构造而不是 make_shared,则给出警告。

    R.23:使用 make_unique() 创建 unique_ptr

    make_unique 提供了更简洁的构造语句。它还确保在复杂表达式中的异常安全。

    make_unique 是 C++14 引入的,而 make_shared 在 C++11 就已经有了

    示例

    // 可行,但重复出现 Foo
    unique_ptr p {new Foo{7}};
    // 更好的写法:避免重复的 Foo
    auto q = make_unique(7);
    

    代码检查

    如果一个 unique_ptr 是由 new 的结果构造而不是 make_unique,则给出警告。

    R.24:使用 std::weak_ptr 打破 shared_ptr 的循环引用

    shared_ptr 依赖于引用计数,而循环结构的引用计数永远不为 0,因此我们需要一种机制来打破循环结构。

    例子

    #include 
    
    class bar;
    
    class foo {
    public:
      explicit foo(const std::shared_ptr& forward_reference)
        : forward_reference_(forward_reference)
      { }
    private:
      std::shared_ptr forward_reference_;
    };
    
    class bar {
    public:
      explicit bar(const std::weak_ptr& back_reference)
        : back_reference_(back_reference)
      { }
      void do_something()
      {
        if (auto shared_back_reference = back_reference_.lock()) {
          // 使用*shared_back_reference
        }
      }
    private:
      std::weak_ptr back_reference_;
    }
    

    Herb Sutter:有很多人说“打破循环引用”,但我认为“临时共享所有权”更准确。

    Bjarne Stroustrup:“打破循环”是必须要做的,“临时共享所有权”是你如何“打破循环”。你可以通过使用另一个 shared_ptr 来“临时共享所有权”。(这里不太好翻译,贴出原文:breaking cycles is what you must do; temporarily sharing ownership is how you do it. You could “temporarily share ownership” simply by using another shared_ptr

    代码检查

    如果可以静态地检测到循环(可能无法实现),就不需要 weak_ptr。


    R.30: 仅在需要明确表示生命周期语义时才将智能指针作为参数传递

    参见 F.7: 对于一般用途,使用 T* 或 T& 参数而不是智能指针

    R.31: 如果你使用的是非 std 智能指针,遵循 std 的基本模式

    任何重载了一元 *-> 运算符的类型(包括模板及特化模板)都被认为是智能指针:

    • 如果它是可复制的,则应被视为 shared_ptr
    • 如果它不可复制,则应被视为 unique_ptr

    反面例子

    // Boost 的 intrusive_ptr
    #include 
    void f(boost::intrusive_ptr p)
    {
        p->foo();
    }
    
    // Microsoft 的 CComPtr
    #include 
    void f(CComPtr p)
    {
        p->foo();
    }
    

    p 是一个共享指针,但在这里没有用到它的共享性,并且通过值传递会导致性能下降;函数只有在需要参与 widget 的生命周期管理的时候使用智能指针。否则,应该使用 widget& 或者 widget*(如果实参可能是 nullptr)作为形参。

    这些第三方/自定义的智能指针与 std::shared_ptr 概念一致,因此接下来的规则也适用于其他类型的第三方和自定义智能指针。这对于排查常见的智能指针错误、性能问题时非常有用。

    R.32: 将形参声明为 unique_ptr 来表明函数对 widget 的所有权

    R.33: 将形参声明为 unique_ptr& 来表明函数对 widget 的重新赋值语义

    以这种方式使用 unique_ptr 既可以起到 self-documented 的作用,又可以强制执行函数调用的所有权转移或重新赋值(reseat)语义。

    注意:“重新赋值”(reseat)的意思是:使指针或智能指针指向不同的对象。

    例子

    // 接受 widget 的所有权
    void sink(unique_ptr);
    // 仅使用 widget
    void uses(widget*);
    // 可能重新赋值指针
    void reseat(unique_ptr&);
    

    反面例子

    // 通常不是你想要的
    void thinko(const unique_ptr&);
    

    代码检查

    • 如果一个函数通过左值引用接受 unique_ptr 参数,并且在某条代码路径上没有对其进行赋值或调用 reset(),则给出警告。建议改为接受 T*T&
    • 如果一个函数通过 const 引用接受 unique_ptr 参数,则给出警告。建议改为接受 const T* 或 const T&。

    R.34: 将形参声明为 shared_ptr 来明确表明函数共享所有权

    R.35: 将形参声明为 shared_ptr& 来表明函数可能重新赋值共享指针

    R.36: 将形参声明为 const shared_ptr& 来表明它可能增加对象的引用计数

    注:“重新赋值”(reseat)的意思是:使引用或智能指针指向不同的对象。

    例子

    class WidgetUser
    {
    public:
        // WidgetUser 将共享 widget 的所有权
        explicit WidgetUser(std::shared_ptr w) noexcept:
            m_widget{std::move(w)} {}
        // ...
    private:
        std::shared_ptr m_widget;
    };
    
    void ChangeWidget(std::shared_ptr& w)
    {
        // 改变调用者的 widget
        w = std::make_shared(widget{});
    }
    
    // 共享所有权,增加引用计数
    void share(shared_ptr);
    // 可能重新赋值指针
    void reseat(shared_ptr&);
    // 可能增加引用计数
    void may_share(const shared_ptr&);
    

    代码检查

    • 如果函数通过左值引用接受 shared_ptr 参数,并且在某条代码路径上没有对其进行赋值或调用 reset(),则给出警告。建议改为接受 T* 或 T&。
    • 如果函数通过值传递或 const 引用接受 shared_ptr 参数,并且在某条代码路径上没有将其复制或移动到另一个 shared_ptr,则给出警告。建议改为接受 T* 或 T&。
    • 如果函数通过右值引用接受 shared_ptr 参数,建议改为值传递。

    R.37: 不要传递从“智能指针别名”取得的指针或引用

    违反本规则是导致丢失引用计数、产生悬空指针的首要原因。函数应优先考虑传递裸指针或引用到调用链的下游。在调用树的顶部,从智能指针取得裸指针或引用时,需要确保智能指针在调用树内部不会无意中被重置或重新赋值。

    注:有时需要对智能指针进行本地拷贝,以保证在函数调用树的整个期间保持对象不被释放。

    例子

    // 全局(静态或堆),或者本地智能指针的别名
    shared_ptr g_p = ...;
    
    void f(widget& w)
    {
        g();
        use(w);  // A
    }
    
    void g()
    {
        // 如果这是最后一个指向 widget 的 shared_ptr,销毁 widget
        g_p = ...;
    }
    

    下面的代码无法通过 code review

    void my_code()
    {
        // BAD: 传递一个“从非本地智能指针获得的指针或引用”,可能在 f(或 f 调用的函数)中不小心被重置
        f(*g_p);
    
        // BAD: 原因同上,只是作为 "this" 指针传递
        g_p->func();
    }
    

    解决办法:拷贝一份副本,确保函数调用树整个期间引用计数不为 0

    void my_code()
    {
        // 引用计数增加了 1,可以覆盖整个函数执行及调用树
        auto pin = g_p;
    
        // GOOD: 传递一个从“本地非别名的指针指针”获得的指针或引用
        f(*pin);
    
        // GOOD: 同上
        pin->func();
    }
    

    代码检查

    • 如果在函数调用过程中使用的指针或引用是从一个非本地的 shared_ptr 或 unique_ptr 取得,或者从一个本地、但可能是别名的智能指针取得,给出警告。
    • 如果是 shared_ptr,建议保存一个本地副本,然后从该副本获取指针或引用。
  • 相关阅读:
    Qt常用快捷键
    CMake中add_executable的使用
    差分信号的末端并联电容到底有什么作用?
    Android图片圆角转换 RoundedImageView开源项目 小记(1)
    基于SSM的班级事务管理系统
    Golang | Leetcode Golang题解之第50题Pow(x,n)
    Oozie 集成 Hive
    第七章 指针
    Maven仓库地址(寻找Maven依赖)
    二叉树的前序、中序、后序、层序遍历
  • 原文地址:https://www.cnblogs.com/tengzijian/p/17516959.html