Advanced C++: shared_ptr of smart pointers

Continue to create, accelerate growth! This is the first day of my participation in the "Nuggets Daily New Plan · June Update Challenge", click to view the details of the event

std::shared_ptr concept

Because of its limitations (exclusive ownership), unique_ptr is rarely used for multi-threaded operations. In multi-threaded operation, resources can be shared and resources can be automatically released, which introduces shared_ptr.

In order to support cross-thread access, shared_ptr has a reference count (thread safety) inside, which is used to record the number of shared_ptrs currently using the resource. Automatically release its associated resources.

Features In contrast to the exclusive ownership of unique_ptr, shared_ptr can share ownership. There is a reference count inside it, which is used to record the number of shared_ptr that shares the resource. When the shared count is 0, the associated resource will be released automatically.

Compared with unique_ptr, shared_ptr does not support arrays, so if you use shared_ptr to point to an array, you need to manually implement the deleter yourself, as shown below:

std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});
复制代码

shared_ptr template class

template<class T> class shared_ptr {
  public:
    using element_type = remove_extent_t<T>;
    using weak_type    = weak_ptr<T>;
 
    // 构造函数
    constexpr shared_ptr() noexcept;
    constexpr shared_ptr(nullptr_t) noexcept : shared_ptr() { }
    template<class Y> explicit shared_ptr(Y* p);
    template<class Y, class D> shared_ptr(Y* p, D d);
    template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
    template<class D> shared_ptr(nullptr_t p, D d);
    template<class D, class A> shared_ptr(nullptr_t p, D d, A a);
    template<class Y>
    shared_ptr(const shared_ptr<Y>& r, element_type* p) noexcept;
    template<class Y>
    shared_ptr(shared_ptr<Y>&& r, element_type* p) noexcept;
    shared_ptr(const shared_ptr& r) noexcept;
    template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept;
    shared_ptr(shared_ptr&& r) noexcept;
    template<class Y> shared_ptr(shared_ptr<Y>&& r) noexcept;
    template<class Y> explicit shared_ptr(const weak_ptr<Y>& r);
    template<class Y, class D> shared_ptr(unique_ptr<Y, D>&& r);
 
    // 析构函数
    ~shared_ptr();
 
    // 赋值
    shared_ptr& operator=(const shared_ptr& r) noexcept;
    template<class Y>
    shared_ptr& operator=(const shared_ptr<Y>& r) noexcept;
    shared_ptr& operator=(shared_ptr&& r) noexcept;
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y>&& r) noexcept;
    template<class Y, class D>
    shared_ptr& operator=(unique_ptr<Y, D>&& r);
 
    // 修改函数
    void swap(shared_ptr& r) noexcept;
    void reset() noexcept;
    template<class Y> void reset(Y* p);
    template<class Y, class D> void reset(Y* p, D d);
    template<class Y, class D, class A> void reset(Y* p, D d, A a);
 
    // 探察函数
    element_type* get() const noexcept;
    T& operator*() const noexcept;
    T* operator->() const noexcept;
    element_type& operator[](ptrdiff_t i) const;
    long use_count() const noexcept;
    explicit operator bool() const noexcept;
    template<class U>
    bool owner_before(const shared_ptr<U>& b) const noexcept;
    template<class U>
    bool owner_before(const weak_ptr<U>& b) const noexcept;
  };
复制代码

shared_ptr multiple pointers to the same object. shared_ptr uses reference counting, each copy of shared_ptr points to the same memory. Each time it is used, the internal reference count increases by 1, and each time it is destructed, the internal reference count decreases by 1. When it is reduced to 0, the pointed heap memory is automatically deleted. The reference count inside shared_ptr is thread-safe, but reading the object requires locking.

  • initialization. A smart pointer is a template class that can specify a type, and the incoming pointer is initialized through the constructor. It can also be initialized using the make_shared function. You cannot assign a pointer directly to a smart pointer, one for a class and one for a pointer. For example std::shared_ptr<int> p4 = new int(1);, the spelling is wrong and cannot be implicitly converted.
  • copy and assignment. Copying increases the reference count of the object by 1, and assignment causes the reference count of the original object to decrease by 1. When the count is 0, the memory is automatically released. The reference count of the object pointed to later is incremented by 1, pointing to the later object.
  • The get function gets a raw pointer.
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用我们在后面的weak_ptr中介绍。

所有智能指针类都有一个explicit构造函数,该构造函数将指针作为参数。因此不需要自动将指针转换为智能指针对象:

std::shared_ptr<int> pi;
int* p_reg = new int;
//pi = p_reg;  // not allowed(implicit conversion)
pi = std::shared_ptr<int>(p_reg);  // allowed(explicit conversion)
//std::shared_ptr<int> pshared = p_reg;  // not allowed(implicit conversion)
//std::shared_ptr<int> pshared(g_reg);  // allowed(explicit conversion)
复制代码

下面我们看一个简单的例子:

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    std::shared_ptr<int> sp = std::make_shared<int>(10);
    cout << sp.use_count() << endl;//1
    std::shared_ptr<int> sp1(sp);//再次被引用则计数+1
    cout << sp1.use_count() << endl;//2
}
复制代码

从上面可以看到,多次被引用则会增加计数,我们可以通过使用use_count方法打印具体的计数。

shared_ptr的构造和析构

#include <iostream>
#include <memory>

struct C {int* data;};

int main () {
 auto deleter = [](int* ptr){
    std::cout << "custom deleter called\n";
    delete ptr;
  };//Labmbda表达式

  //默认构造,没有获取任何指针的所有权,引用计数为0
  std::shared_ptr<int> sp1;
  std::shared_ptr<int> sp2 (nullptr);//同1
  //拥有指向int的指针所有权,引用计数为1
  std::shared_ptr<int> sp3 (new int);
  //同3,但是拥有自己的析构方法,如果指针所指向对象为复杂结构C
  //结构C里有指针,默认析构函数不会将结构C里的指针data所指向的内存释放,
  //这时需要自己使用自己的析构函数(删除器)
  std::shared_ptr<int> sp4 (new int, deleter);
  //同4,但拥有自己的分配器(构造函数),
  //如成员中有指针,可以为指针分配内存,原理跟浅拷贝和深拷贝类似                         
  std::shared_ptr<int> sp5 (new int, [](int* p){delete p;}, std::allocator<int>());
  //如果p5引用计数不为0,则引用计数加1,否则同样为0, p6为0
  std::shared_ptr<int> sp6 (sp5);
  //p6的所有权全部移交给p7,p6引用计数变为为0
  std::shared_ptr<int> sp7 (std::move(sp6));
  //p8获取所有权,引用计数设置为1
  std::shared_ptr<int> sp8 (std::unique_ptr<int>(new int));
  std::shared_ptr<C> obj (new C);
  //同6一样,只不过拥有自己的删除器与4一样
  std::shared_ptr<int> sp9 (obj, obj->data);

  std::cout << "use_count:\n";
  std::cout << "p1: " << sp1.use_count() << '\n'; //0
  std::cout << "p2: " << sp2.use_count() << '\n'; //0
  std::cout << "p3: " << sp3.use_count() << '\n'; //1
  std::cout << "p4: " << sp4.use_count() << '\n'; //1
  std::cout << "p5: " << sp5.use_count() << '\n'; //2
  std::cout << "p6: " << sp6.use_count() << '\n'; //0
  std::cout << "p7: " << sp7.use_count() << '\n'; //2
  std::cout << "p8: " << sp8.use_count() << '\n'; //1
  std::cout << "p9: " << sp9.use_count() << '\n'; //2
  return 0;
}
复制代码

shared_ptr赋值

给shared_ptr赋值有三种方式,如下

#include <iostream>
#include <memory>

int main () {
  std::shared_ptr<int> foo;
  std::shared_ptr<int> bar (new int(10));
  //右边是左值,拷贝赋值,引用计数加1
  foo = bar; 
  //右边是右值,所以是移动赋值
  bar = std::make_shared<int> (20); 
  //unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,
  //无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr
  std::unique_ptr<int> unique (new int(30));
  // move from unique_ptr,引用计数转移
  foo = std::move(unique); 

  std::cout << "*foo: " << *foo << '\n';
  std::cout << "*bar: " << *bar << '\n';

  return 0;
}
复制代码

make_shared

看下面make_shared的用法:

#include <iostream>
#include <memory>

int main () {

  std::shared_ptr<int> foo = std::make_shared<int> (10);
  // same as:
  std::shared_ptr<int> foo2 (new int(10));
  //创建内存,并返回共享指针,只创建一次内存
  auto bar = std::make_shared<int> (20);

  auto baz = std::make_shared<std::pair<int,int>> (30,40);

  std::cout << "*foo: " << *foo << '\n';
  std::cout << "*bar: " << *bar << '\n';
  std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';

  return 0;
}
复制代码

效率提升 std::make_shared(比起直接使用new)的一个特性是能提升效率。使用std::make_shared允许编译器产生更小,更快的代码,产生的代码使用更简洁的数据结构。考虑下面直接使用new的代码:

std::shared_ptr<Test> sp(new Test);
复制代码

很明显这段代码需要分配内存,但是它实际上要分配两次。每个std::shared_ptr都指向一个控制块,控制块包含被指向对象的引用计数以及其他东西。这个控制块的内存是在std::shared_ptr的构造函数中分配的。因此直接使用new,需要一块内存分配给Widget,还要一块内存分配给控制块。如果使用std::make_shared来替换:

auto sp = std::make_shared<Test>();
复制代码

一次分配就足够了。这是因为std::make_shared申请一个单独的内存块来同时存放Widget对象和控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。

对std::make_shared的效率分析可以同样地应用在std::allocate_shared上,所以std::make_shared的性能优点也可以扩展到这个函数上。

异常安全

另外一个std::make_shared的好处是异常安全,我们看下面一句简单的代码:

callTest(std::shared_ptr<Test>(new Test), secondFun());
复制代码

简单说,上面这个代码可能会发生内存泄漏,我们先来看下上面这个调用中几个语句的执行顺序,可能是顺序如下:

new Test()
secondFun()
std::shared_ptr<Test>()
复制代码

如果真是按照上面这样的代码顺序执行,那么在运行期,如果secondFun()中产生了一个异常,程序就会直接返回了,则第一步new Test分配的内存就泄露了,因为它永远不会被存放到在第三步才开始管理它的std::shared_ptr中。但是如果使用std::make_shared则可以避免这样的问题。调用代码将看起来像这样:

callTest(std::make_shared<Test>(), secondFun());           
复制代码

在运行期,不管std::make_shared或secondFun哪一个先被调用。如果std::make_shared先被调用,则在secondFun调用前,指向动态分配出来的Test的原始指针能安全地被存放到std::shared_ptr中。如果secondFun之后产生一个异常,std::shared_ptr的析构函数将发现它持有的Test需要被销毁。并且如果secondFun先被调用并产生一个异常,std::make_shared就不会被调用,因此这里就不需要考虑动态分配的Test了。

计数线程安全?

我们上面一直说shared_ptr中的计数是线程安全的,其实shared_ptr中的计数是使用了我们前面文章介绍的std::atomic特性,引用计数加一减一操作是原子性的,所以线程安全的。引用计数器的使用等价于用 std::memory_order_relaxedstd::atomic::fetch_add 自增(自减要求更强的顺序,以安全销毁控制块)。

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
 
struct Test
{
    Test() { std::cout << " Test::Test()\n"; }
    ~Test() { std::cout << " Test::~Test()\n"; }
};
 
//线程函数
void thr(std::shared_ptr<Test> p)
{
    //线程暂停1s
    std::this_thread::sleep_for(std::chrono::seconds(1));

    //赋值操作, shared_ptr引用计数use_cont加1(c++11中是原子操作)
    std::shared_ptr<Test> lp = p;
    {
        //static变量(单例模式),多线程同步用
        static std::mutex io_mutex;

        //std::lock_guard加锁
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
            << " lp.get() = " << lp.get()
            << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}
 
int main()
{
    //使用make_shared一次分配好需要内存
    std::shared_ptr<Test> p = std::make_shared<Test>();
    //std::shared_ptr<Test> p(new Test);

    std::cout << "Created a shared Test\n"
        << " p.get() = " << p.get()
        << ", p.use_count() = " << p.use_count() << '\n';

    //创建三个线程,t1,t2,t3
    //形参作为拷贝, 引用计数也会加1
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    std::cout << "Shared ownership between 3 threads and released\n"
        << "ownership from main:\n"
        << " p.get() = " << p.get()
        << ", p.use_count() = " << p.use_count() << '\n';
    //等待结束
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted\n";

    return 0;
}
复制代码

输出:

Test::Test()
Created a shared Test
 p.get() = 0xa7cec0, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
 p.get() = 0xa7cec0, p.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 5
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 3
All threads completed, the last one deleted
 Test::~Test()
复制代码

enable_shared_from_this

在某些场合下,会遇到一种情况,如何安全的获取对象的this指针,一般来说我们不建议直接返回this指针,可以想象下有这么一种情况,返回的this指针保存在外部一个局部或全局变量,当对象已经被析构了,但是外部变量并不知道指针指向的对象已经被析构了,如果此时外部继续使用了这个指针就会发生程序奔溃。既要像指针操作对象一样,又能安全的析构对象,很自然就想到,智能指针就很合适!我们来看下面这段程序:

#include <iostream>
#include <memory>

class Test{
public:
    Test(){
        std::cout << "Test::Test()" << std::endl;
    }
    ~Test(){
        std::cout << "Test::~Test()" << std::endl;
    }

    std::shared_ptr<Test> GetThis(){
        return std::shared_ptr<Test>(this);
    }
};

int main()
{
    std::shared_ptr<Test> p(new Test());
    std::shared_ptr<Test> p_this = p->GetThis();

    std::cout << p.use_count() << std::endl;
    std::cout << p_this.use_count() << std::endl;

    return 0;
}
复制代码

编译运行后程序输出如下:

free(): double free detected in tcache 2
Test::Test()
1
1
Test::~Test()
Test::~Test()
复制代码

从上面的输出可以看到,构造函数调用了一次,析构函数却调用了两次,很明显这是不正确的。而std::enable_shared_from_this正是为了解决这个问题而存在。

std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, ... ) ,它们与 pt 共享对象 t 的所有权(这个是关键,直接使用this无法达到该效果)。

std::enable_shared_from_this是模板类,内部有个_Tp类型weak_ptr指针,std::enable_shared_from_this的构造函数都是protected,因此不能直接创建std::enable_from_shared_from_this类的实例变量,只能作为基类使用,通过调用shared_from_this成员函数,将会返回一个新的 std::shared_ptr<T> 对象,它与 pt 共享 t 的所有权。因此使用方法如下代码所示:

#include <iostream>
#include <memory>

// 这里必须要 public继承,除非用struct
class Test : public std::enable_shared_from_this<Test> {
public:
    Test(){
        std::cout << "Test::Test()" << std::endl;
    }
    ~Test(){
        std::cout << "Test::~Test()" << std::endl;
    }

    std::shared_ptr<Test> GetThis(){
        std::cout << "shared_from_this()" << std::endl;
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Test> p(new Test());
    std::shared_ptr<Test> p_this = p->GetThis();

    std::cout << p.use_count() << std::endl;
    std::cout << p_this.use_count() << std::endl;

    return 0;
}
复制代码

Construct an object through the enable_shared_from_thisdefined , which can share the Test object with other shared_ptr. Generally, we use it in asynchronous threads. In asynchronous calls, there is a keep-alive mechanism. We cannot determine the time when asynchronous functions are executed. However, asynchronous functions may use variables that existed before the asynchronous call. In order to ensure that the variable is always valid during the execution of the asynchronous function, we can pass a share_ptr pointing to itself to the asynchronous function, so that the object managed by share_ptr will not be destructed during the execution of the asynchronous function, and the variables used will always be valid. (keep alive).shared_from_this()shared_ptr<Test>

Notes on using shared_ptr:

  • Do not manage a native pointer to multiple shared_ptr; do not actively delete shared_ptrthe managed raw pointer;

    BigObj *p = new BigObj();
    std::shared_ptr<BigObj> sp(p);
    std::shared_ptr<BigObj> sp1(p);
    delete p;
    复制代码
  • Don't give this pointer to shared_ptr, use enable_shared_from_this as above;

  • Do not replace pointers with shared_ptr without thinking to prevent memory leaks, shared_ptr is not a panacea, and using them also requires a certain overhead;

  • Objects with shared ownership generally live longer than scoped objects, resulting in higher average resource usage times;

  • Using shared pointers in a multithreaded environment is expensive because you need to avoid data races on reference counts;

  • If you use smart pointers to manage resources other than new allocated memory, remember to pass it a deleter.

Summarize

Smart pointers are template classes not pointers. When creating a smart pointer, it must be of the type that the pointer can point to, <int>, <string>... etc. The essence of a smart pointer is a class that overloads the ->AND *operator. The class manages the memory to ensure that even if an exception occurs, the memory can be released through the destructor of the smart pointer class. Specifically, it utilizes reference counting technology and C++'s RAII (resource acquisition is initialization) feature. Okay, let's learn weak_ptr in the next article.

Guess you like

Origin juejin.im/post/7102684262737903653