首页 > 编程语言 >C++ 核心指南之资源管理(下)—— 智能指针最佳实践

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

时间:2023-07-01 23:45:41浏览次数:45  
标签:make C++ 资源管理 void shared unique ptr 指针

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> 来表明函数对 widget 的所有权
  • R.33: 将形参声明为 unique_ptr<widget>& 来表明函数对 widget 的重新赋值语义
  • R.34: 将形参声明为 shared_ptr<widget> 来明确表明函数共享所有权
  • R.35: 将形参声明为 shared_ptr<widget>& 来表明函数可能重新赋值共享指针
  • R.36: 将形参声明为 const shared_ptr<widget>& 来表明它可能增加对象的引用计数
  • R.37: 不要传递从“智能指针别名”取得的指针或引用

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

可以避免资源泄露

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

代码检查

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

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

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

反面例子

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

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

例子

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

代码检查

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

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

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

示例

shared_ptr<X> p1 { new X{2} }; // BAD
auto p = make_shared<X>(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<Foo> p {new Foo{7}};
// 更好的写法:避免重复的 Foo
auto q = make_unique<Foo>(7);

代码检查

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

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

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

例子

#include <memory>

class bar;

class foo {
public:
  explicit foo(const std::shared_ptr<bar>& forward_reference)
    : forward_reference_(forward_reference)
  { }
private:
  std::shared_ptr<bar> forward_reference_;
};

class bar {
public:
  explicit bar(const std::weak_ptr<foo>& back_reference)
    : back_reference_(back_reference)
  { }
  void do_something()
  {
    if (auto shared_back_reference = back_reference_.lock()) {
      // 使用*shared_back_reference
    }
  }
private:
  std::weak_ptr<foo> 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 <boost/intrusive_ptr.hpp>
void f(boost::intrusive_ptr<widget> p)
{
    p->foo();
}

// Microsoft 的 CComPtr
#include <atlbase.h>
void f(CComPtr<widget> p)
{
    p->foo();
}

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

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

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

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

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

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

例子

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

反面例子

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

代码检查

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

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

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

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

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

例子

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

代码检查

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

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

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

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

例子

// 全局(静态或堆),或者本地智能指针的别名
shared_ptr<widget> 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,建议保存一个本地副本,然后从该副本获取指针或引用。

标签:make,C++,资源管理,void,shared,unique,ptr,指针
From: https://www.cnblogs.com/tengzijian/p/17516959.html

相关文章

  • C++面试八股文:技术勘误
    不知不觉,《C++面试八股文》已经更新30篇了,这是我第一次写技术博客,由于个人能力有限,出现了不少纰漏,在此向各位读者小伙伴们致歉。为了不误导更多的小伙伴,以后会不定期的出勘误文章,请各位小伙伴留意。在《C++面试八股文:C++中,设计一个类要注意哪些东西?》一文中,#include<iostream>......
  • 【C++】打开C++大门,踏入C++世界
    ☑️前言......
  • C++面试八股文:std::vector和std::list,如何选择?
    C++面试八股文:std::vector和std::list,如何选择?某日二师兄参加XXX科技公司的C++工程师开发岗位第24面:面试官:list用过吗?二师兄:嗯,用过。面试官:请讲一下list的实现原理。二师兄:std::list被称为双向链表,和C中手写双向链表本质上没有大的区别。list对象中有两个指针,一个指向......
  • 利用ccache提高c++编译速度
    首先安装ccache:sudoaptinstallccache然后在cmake文件中添加如下代码即可:find_program(CCACHE_FOUNDccache)if(CCACHE_FOUND)set_property(GLOBALPROPERTYRULE_LAUNCH_COMPILEccache)set_property(GLOBALPROPERTYRULE_LAUNCH_LINKccache)endi......
  • assert断言与const修饰指针的妙用(模拟实现strcpy函数)
     assert断言目录assert断言的妙用:头文件:使用方法:const修饰指针的妙用主要用法const在*左边const在*右边断言和const修饰指针的应用模拟实现C语言strcpy函数  1、若字符串str1,str2有空指针怎么办?  2.str2改变了怎么办?assert断言的妙用:头文件:#include<assert.h>使用方法:当......
  • C/C++《数据结构课程设计》题目[2023-07-01]
    C/C++《数据结构课程设计》题目[2023-07-01]《数据结构课程设计》题目一、【大数四则运算】——线性表[习题描述]设计—个实现任意长的整数进行四则运算和幂次运算的演示程序。[基本要求]利用双向循环链表实现大数的存储,每个结点含一个整型变量。[实现提示]实现原理:任何一......
  • C++ 编程中的核心知识点
    const作用修饰变量,说明该变量不可以被改变;修饰指针,分为指向常量的指针(pointertoconst)和自身是常量的指针(常量指针,constpointer);修饰引用,指向常量的引用(referencetoconst),用于形参类型,即避免了拷贝,又避免了函数对值的修改;修饰成员函数,说明该成员函数内不能修改成员......
  • C++之future
    背景在C++多线程编程中,同步线程间的操作和结果通常是一个关键问题。C++11引入了std::future这一同步原语,用于表示异步操作的结果。本文将介绍C++中std::future的使用方法、优势以及与其他同步方法的对比。使用std::futurestd::future表示一个异步操作的结果,可以用于获取操作的......
  • Qt/C++编写超精美自定义控件(历时9年更新迭代/超202个控件/祖传原创)
    一、前言无论是哪一门开发框架,如果涉及到UI这块,肯定需要用到自定义控件,越复杂功能越多的项目,自定义控件的数量就越多,最开始的时候可能每个自定义控件都针对特定的应用场景,甚至里面带了特定的场景的一些设置和处理,随着项目数量的增多,有些控件又专门提取出来共性,做成了通用的自定义......
  • 【零基础学习C++】如何写一个C++类?
    个人主页:【......