智能指针与所有权 Shepard-Wang

一 起因:资源管理

对资源(内存,文件描述符,网络套接字,互斥量)进行管理首先要明确的就是 所有权

对管理资源的类进行浅拷贝要小心谨慎,这可能会造成所有权的混乱!因此一般有几种解决办法:

  1. 总是深复制。但是随之而来的开销也是巨大的。
  2. 单一所有权。 禁止复制,禁止编译器自动生成 copy construct/operator=,如 将上述两种函数声明为=delete ,或生命在 private 访问限定符内。unique_ptr 采用这种方式实现。
  3. 共享所有权。 通过引用计数实现, shared_ptr 就是采用这种方式实现。
  4. 不拥有所有权。只读访问。比如 string_viewweak_ptr 就是采用这种方式实现。

二 手法:RAII

RAII(Resource Acquisition Is Initialization)是一种 利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 避免资源泄露,不需要显式地释放资源。

  • 避免悬空指针,采用这种方式,对象所需的资源在其生命期内始终保持有效。

通过在类内重载 operator*operator-> 让智能指针对象可以像指针一样使用。

1.auto_ptr

两个 auto_ptr 不能指向同一个对象。auto_ptr 之间的赋值实际上是 = 右边的 auto_ptr 失去了资源的所有权。

对拷贝的处理方式是:隐式转移所有权

class auto_ptr{
     
     auto_ptr& operator=(auto_ptr tmp) noexcept {
         // copy and swap技术,这里不展开了
         // 注意当拷贝构造函数构造tmp时,会发生所有权的转移
         tmp.swap(*this);
         return *this;
     }
     
     // 拷贝构造,被复制时释放原来指针的所有权,交给复制方
     auto_ptr(auto_ptr &other) noexcept {
         ptr_ = other.release();
     }
     
     // 原来的指针释放所有权
     T *release() noexcept {
         T *ptr = ptr_;
         ptr_ = nullptr;
         return ptr;
     }
 };

缺陷

  1. auto_ptr 这种不能复制的特点不适用于 STL 一些算法和容器。

  2. 不能托管数组。auto_ptr<int[]> 是不支持的。

  3. 这个拷贝是非常致命的,也就是发生拷贝时,old auto_ptr 的所有权转交到 new auto_ptr 手上。导致 old auto_ptr 悬空了。

     auto_ptr<int> ptr1{new int};
     auto_ptr<int> ptr2{ptr1}; // ptr1变空指针了
    

2. unique_ptr

原理与实现

对拷贝的处理方式是:不允许隐式转移所有权,必须显式移动语句才能转移所有权

为实现上述功能有两点需要注意:

  1. 拷贝构造函数和赋值运算符重载函数必须禁止。
  2. 需要支持移动构造函数,移动赋值运算符重载函数。

移动拷贝构造和移动赋值运算符重载会抑制编译器生成默认的拷贝构造和赋值运算符重载函数。

class unique_ptr{
 public:
     unique_ptr(unique_ptr &&other) noexcept { // 所有权转移
         ptr_ = other.release();
     }
 
     // copy and swap  
     // 在构造参数tmp时,只能调用移动构造函数
     unique_ptr &operator=(unique_ptr tmp) noexcept {
         tmp.swap(*this);
         return *this;
     }
 
     // 原来的指针释放所有权
     T *release() noexcept {
         T *ptr = ptr_;
         ptr_ = nullptr;
         return ptr;
     }
 
     void swap(unique_ptr &rhs) noexcept {
         using std::swap;
         swap(ptr_, rhs.ptr_);    // 转移指针所有权
     }
 };

优势

  1. 不支持复制语义, auto_ptr 的替代品

  2. 可以用于不支持复制语义的 STL 容器与算法

    std::vector<std::unique_ptr<int>> v;
    std::unique_ptr<int> pi = std::make_unique<int>();
    *pi = 3;
    // v.push_back(pi); error!
    v.emplace_back(std::make_unique<int>(3));
    v.push_back(std::move(pi));
    
  3. 时间和空间上对比 raw 指针没有太大额外开销

    我们知道 unique_ptr 可以指定 deletordeletor 作为一个模板参数传入,类内应该有一个 deletor 类型的成员才对。为什么没有额外的开销?实际上我的 gccunique_ptr 底层使用 tuple 实现的。应该是继承了 包含deletor d 成员的类,由于这个类是不含成员空类,编译器做了 EBO,空基类优化!

  4. 可以定制 Deletor

    std::unique_ptr<T, Deletor>
    
  5. 对数组类型有一个特化版本

    std::unique_ptr<T[]>
    

3 .shared_ptr

  1. 引用计数
  2. control block 线程安全,资源不保证安全。
  3. 定制 deletor

raii31.png

使用

void* operator new(size_t sz)
{
    cout << "need space size: " << sz << endl;
    return malloc(sz);
}

// 两次内存申请
shared_ptr<int> sp(new int(10));

// 一次内存申请
shared_ptr<int> sp2 = make_shared<int>(10);

标准输出:
need space size: 4
need space size: 24
need space size: 24

定制 Deletor

template<typename T>
class Deleter
{
public:
    void operator()(T* ptr)
    {
        ++Deleter::count;
        delete ptr;
    }

    void getInfo()
    {
        std::string typeID{typeid(T).name()};
        size_t sz = Deleter::count * sizeof(T);
        cout << "Delete " << Deleter::count << " objects of " << typeID << endl;
        cout << "Free size in bytes: " << sz << endl;
    }

private:
    static int count;
};

template<typename T>
int Deleter<T>::count = 0;

typedef Deleter<int> IntDeleter;


int main()
{
    std::shared_ptr<int> sp1(new int(1000), IntDeleter());
    auto del = std::get_deleter<IntDeleter>(sp1);
    del->getInfo();
    sp1.reset();
    del->getInfo();
}

4. weak_ptr

1) 语义和接口

  1. 不持有资源
  2. shared_ptr 获取资源
  3. 不能直接访问资源
  4. 可以创建一个持有资源的 shared_ptr

weak_ptr 并不增加 ref count (会增加 weak count),可以用来破除 shared_ptr 的循环引用带来的内存泄漏问题。

raii30.png

int main()
{
    cout << std::boolalpha;

    std::shared_ptr<int> sp1 = std::make_shared<int>(1000);
    std::weak_ptr<int> wp1 = sp1;

    cout << "sp1 ref count: " << sp1.use_count() << endl;
    cout << "wp1 ref count: " << wp1.use_count() << endl;
    cout << "wp1 expired? "   << wp1.expired()   << endl;

    if( std::shared_ptr<int> sp2 = wp1.lock() )
    {
        cout << "*sp2 = " << *sp2 << endl;
        cout << "sp2 ref count: " << sp2.use_count() << endl;
    }
    else
        cout << "wp1 expired! Don't use that resourse" << endl;

    wp1.reset();

    if( std::shared_ptr<int> sp2 = wp1.lock() )
    {
        cout << "*sp2 = " << *sp2 << endl;
        cout << "sp2 ref count: " << sp2.use_count() << endl;
    }
    else
        cout << "wp1 expired! Don't use that resourse" << endl;
}

输出:

[global operator new] size = 24
sp1 ref count: 1
wp1 ref count: 1
wp1 expired? false
*sp2 = 1000
sp2 ref count: 2
wp1 expired! Don't use that resourse
[global operator delete] ptr = 0x197390018e0

2)weak_ptr 破除循环引用!

raii32.png

raii33.png

class Son;
class Daughter;

class Mother
{
public:
    ~Mother() { cout << "Mother gone" << endl; }
    void setSon(std::shared_ptr<Son> s) { son = s; }
    void setDaughter(std::shared_ptr<Daughter> d) { daughter = d; }
private:
    std::shared_ptr<Son> son;
    std::weak_ptr<Daughter> daughter;
};

class Son
{
public:
    Son(std::shared_ptr<Mother> m) : mother(m) {}
    ~Son() { cout << "Son gone" << endl; }
private:
    std::shared_ptr<Mother> mother;
};

class Daughter
{
public:
    Daughter(std::shared_ptr<Mother> m) : mother(m) {}
    ~Daughter() { cout << "Daughter gone" << endl; }
private:
    std::shared_ptr<Mother> mother;
};

int main()
{

    std::shared_ptr<Mother> mother = std::make_shared<Mother>();
    std::shared_ptr<Son> son = std::make_shared<Son>(mother);
    std::shared_ptr<Daughter> daughter = std::make_shared<Daughter>(mother);
    mother->setSon(son);
    mother->setDaughter(daughter);
}

输出:

[global operator new] size = 48
[global operator new] size = 32
[global operator new] size = 32
Daughter gone

当我们将 Mother 中堆 Son 的引用变为 weak_ptr 后,MotherSon 之间的循环引用就被打破

class Mother
{
public:
    ~Mother() { cout << "Mother gone" << endl; }
    void setSon(std::shared_ptr<Son> s) { son = s; }
    void setDaughter(std::shared_ptr<Daughter> d) { daughter = d; }
private:
    std::weak_ptr<Son> son;
    std::weak_ptr<Daughter> daughter;
};

输出:

[global operator new] size = 48
[global operator new] size = 32
[global operator new] size = 32
Daughter gone
Son gone
Mother gone
[global operator delete] ptr = 0x1cdcd0f1a90
[global operator delete] ptr = 0x1cdcd0f1a60
[global operator delete] ptr = 0x1cdcd0f18e0

3)使用场景

std::weak_ptr 的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。

例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。

std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。

class Subscriber
 {
 
 };
 
 class SubscribeManager
 {
 public:
     void publish()
     {
         for (const auto& iter : m_subscribers)
         {
             if (!iter.expired())
             {
                 //TODO:给订阅者发送消息
             }
         }
     }
 
 private:
     std::vector<std::weak_ptr<Subscriber>>   m_subscribers;
 };

4)对象模型

smart-pointer16.png

refcount = 0时,weak refcount != 0control block 不会被释放,整块内存不会归还给系统,可能会因为存在 weak_ptr 而导致内存泄漏。如果程序中存在大量,weak_ptr 可以考虑自己手写 make_shared 避免该情况。

unique_ptrshared_ptr 的空基类优化

unique_ptr

class unique_ptr
{
    __uniq_ptr_data<_Tp, _Dp> _M_t;
};

struct __uniq_ptr_data : __uniq_ptr_impl<_Tp, _Dp> {};

class __uniq_ptr_impl {   tuple<pointer, _Dp> _M_t; }  

shared_ptr

template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
{
    class _Impl : _Sp_ebo_helper<0, _Deleter>, _Sp_ebo_helper<1, _Alloc>
    {
        typedef _Sp_ebo_helper<0, _Deleter>	_Del_base;
        typedef _Sp_ebo_helper<1, _Alloc>	_Alloc_base;

        public:
        _Impl(_Ptr __p, _Deleter __d, const _Alloc& __a) noexcept
            : _Del_base(std::move(__d)), _Alloc_base(__a), _M_ptr(__p)
            { }

        _Deleter& _M_del() noexcept { return _Del_base::_S_get(*this); }
        _Alloc& _M_alloc() noexcept { return _Alloc_base::_S_get(*this); }

        _Ptr _M_ptr;
    };
    
      private:
        _Impl _M_impl;
};

_Sp_ebo_helper

// 如果 Tp 不是 final 并且是空,采用 EBO
template<int _Nm, typename _Tp,
bool __use_ebo = !__is_final(_Tp) && __is_empty(_Tp)>
    struct _Sp_ebo_helper;

/// Specialization using EBO.
template<int _Nm, typename _Tp>
struct _Sp_ebo_helper<_Nm, _Tp, true> : private _Tp
{
    explicit _Sp_ebo_helper(const _Tp& __tp) : _Tp(__tp) { }
    explicit _Sp_ebo_helper(_Tp&& __tp) : _Tp(std::move(__tp)) { }

    static _Tp&
        _S_get(_Sp_ebo_helper& __eboh) { return static_cast<_Tp&>(__eboh); }
};

/// Specialization not using EBO.
template<int _Nm, typename _Tp>
struct _Sp_ebo_helper<_Nm, _Tp, false>
{
    explicit _Sp_ebo_helper(const _Tp& __tp) : _M_tp(__tp) { }
    explicit _Sp_ebo_helper(_Tp&& __tp) : _M_tp(std::move(__tp)) { }

    static _Tp&
        _S_get(_Sp_ebo_helper& __eboh)
    { return __eboh._M_tp; }

    private:
    _Tp _M_tp;
};

四 使用智能指针注意事项

  1. 使用智能指针托管的对象,尽量不要在再使用原生指针

    对同一个对象会混用智能指针和原生指针,导致程序异常,所以 get() 方法要少用。

    void incorrect_smart_pointer1()
     {
         A *a= new A();
         std::unique_ptr<A> unique_ptr_a(a);
         // 此处将导致对象的二次释放
         delete a;
     }
    
  2. 不要把一个原生指针交给多个智能指针管理

    如果将一个原生指针交个多个智能指针,这些智能指针释放对象时会产生对象的多次销毁。智能指针可不会在构造时,判断你有没有交给其他智能指针。

    void incorrect_smart_pointer2()
     {
         A *a= new A();
         std::unique_ptr<A> unique_ptr_a1(a);
         std::unique_ptr<A> unique_ptr_a2(a);// 此处将导致对象的二次释放
     }
    
  3. 通过值传递,返回。

    shared_ptr<int> share_func(shared_ptr<int> ptr); // 所有权转移
       
    unique_ptr<int> unique_func(unique_ptr<int> ptr);// 所有权共享
    

shared_ptr 实现原理

smart-pointer25.png

shared_ptr1.png

aliasing constructor

template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

别名使用构造函数:构造 shared_ptr ,与 r 的初始值共享所有权信息,但保有无关且不管理的指针 ptr 。若此 shared_ptr 是离开作用域的组中的最后者,则它将调用最初 r 所管理对象的析构函数。然而,在此 shared_ptr 上调用 get() 将始终返回 ptr 的副本。程序员负责确保只要此 shared_ptr 存在,此 ptr 就保持合法,例如在典型使用情况中,其中 ptr 是 r 所管理对象的成员,或是 r.get() 的别名(例如向下转型)

smart-pointer19.png

通过 get() 结构获取到的是智能指针栈上指向资源块的指针。

使用 set() 修改的也是这个指针。

一开始这个指针指向智能指针托管的对象的地址,但是如果通过 set 修改了该指针,该指针会指向别处。真正管理的资源存储在控制块内。

手写注意事项

use_countweak_count 的增减时机

use_count 何时改变:

  • shared_ptr构造时,+1;
  • shared_ptr拷贝赋值时,+1;
  • weak_ptr.lock(),发生 weakshared 的晋级,+1;
  • shared_ptr析构时,-1;

weak_count 何时改变:

  • weak_ptr构造时,+1;
  • weak_ptr拷贝赋值时,+1;
  • weak_ptr析构时,-1;
  • use_count 减为 0 时,weak_count -1;

我们会发现 weak_count 在初始化时会多加一个1,所以当 weak_count 减为 0 时,weak_count 再 -1。这样就平衡了。之所以加 1 是为了保持接口的一致性。

我们知道一个智能指针的实现最起码包括:指针(指向资源)、强引用、弱引用这三个数据成员。

img

所以我们需要释放两块内存,分别是数据块和控制块。

  • 由于弱引用是不控制数据块资源的,所以是否释放数据块仅需观察强引用 use_count,当 use_count 时,释放数据块;
  • 至于控制块的释放,自然是要等 use_count == 0use_count == 0 时,还记得吗,在分析引用计数何时改变时,介绍到 weak_count 初始化会多加 1,当use_count == 0时,再减去 1。因此当 weak_count == 0 时,一定有 use_count == 0,所以只需判断 weak_count == 0 即可。

通过上面的分析,得出一个注意事项,那就是 weak_ptr 在使用前很可能 use_count == 0,导致数据块已经被释放了,因此 weak_ptr 并不控制数据块的生命周期。

make_shared

优点

  • 异常安全:

    void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }
      
    F(std::shared_ptr<Lhs>(new Lhs("foo")),
      std::shared_ptr<Rhs>(new Rhs("bar")));
    

    由于 C++ 允许子表达式以任意顺序执行,一个可能的顺序为:

    1. new Lhs("foo"))
    2. new Rhs("bar"))
    3. std::shared_ptr<Lhs>
    4. std::shared_ptr<Rhs>

    假设当执行到第二步发生了异常,我们的第一步申请的内存就被泄露了。根本原因在于 原生指针没有及时的被 shared_ptr 托管

    解决方法1:写在一行,避免乱序执行

    auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
    auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
    F(lhs, rhs);
    

    更好的方式是使用 make_shared

    F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
    
  • 只需要一次 new 提高效率,还能节省内存。

缺点

由于 make_shared 对于 管理的对象 和 控制块 只有一次堆内存申请,没有办法单独的释放 管理的对象 和 控制块。

![](https://upload-images.jianshu.io/upload_images/14368201-179a5e7efec34eb2.png?imageMogr2/auto-orient/stripimageView2/2/w/480/format/webp)
![](https://upload-images.jianshu.io/upload_images/14368201-344a9f77a40e7d44.png?imageMogr2/auto-orient/stripimageView2/2/w/480/format/webp)
  1. c++11 - Difference in make_shared and normal shared_ptr in C++

  2. C++11 make_shared

make_shared的优化

class A
{
public:
    A() { cout << "A()" << endl; }
    A(A&& a) { cout << "A(&&)" << endl; }

    void* showMeNumber(void*) { cout << rand() << endl; return nullptr; }

    int i;
    char ch;
    double d;
};

void* operator new(size_t sz)
{
    cout << "need space size: " << sz << endl;
    return malloc(sz);
}

// 两次内存申请
shared_ptr<A> sp(new A());

// 一次内存申请
shared_ptr<A> sp2 = make_shared<A>();

输出:

need space size: 16
A()
need space size: 24
need space size: 32
A()

第一次申请了 16 + 24 = 40 字节

第二次只申请了 32 字节,少了 8 字节?刚好是一个指针的大小。

make_shared 施展了 “我知道你在哪” 的优化:

smart-pointer21.png

shared_from_this

引入:

#include <iostream>
#include <memory>

class Widget{
public:
    Widget(){
        std::cout << "Widget constructor run" << std::endl;
    }
    ~Widget(){
        std::cout << "Widget destructor run" << std::endl;
    }

    std::shared_ptr<Widget> GetSharedObject(){
        return std::shared_ptr<Widget>(this);
    }
};

int main()
{
    std::shared_ptr<Widget> p(new Widget());
    std::shared_ptr<Widget> q = p->GetSharedObject();

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

    return 0;
}

GetSharedObject 让同一个资源被 所有权不共享 的多个 shared_ptr 托管!

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

Widget constructor run
1
1
Widget destructor run
Widget destructor run
22:06:45: 程序异常结束。

通过让类继承 std::enable_shared_from_this ,获取该类中的 shared_from_this 方法,完成 this 的获取。

#include <iostream>
#include <memory>

class Widget : public std::enable_shared_from_this<Widget>{
public:
    Widget(){
        std::cout << "Widget constructor run" << std::endl;
    }
    ~Widget(){
        std::cout << "Widget destructor run" << std::endl;
    }

    std::shared_ptr<Widget> GetSharedObject(){
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Widget> p(new Widget());
    std::shared_ptr<Widget> q = p->GetSharedObject();

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

    return 0;
}

enable_shared_from_this

类内部有一个 weak_ptr,当需要 this 时,通过 shared_from_this 完成 weak_ptrshared_ptr 的提升。

template<class _Tp>
class  enable_shared_from_this
{
    mutable weak_ptr<_Tp> __weak_this_;
protected:

    enable_shared_from_this() _NOEXCEPT {}

    enable_shared_from_this(enable_shared_from_this const&) _NOEXCEPT {}

    enable_shared_from_this& operator=(enable_shared_from_this const&) _NOEXCEPT
        {return *this;}

    ~enable_shared_from_this() {}
public:

    shared_ptr<_Tp> shared_from_this()
        {return shared_ptr<_Tp>(__weak_this_);}

    shared_ptr<_Tp const> shared_from_this() const
        {return shared_ptr<const _Tp>(__weak_this_);}

    
    weak_ptr<_Tp> weak_from_this() _NOEXCEPT
       { return __weak_this_; }


    weak_ptr<const _Tp> weak_from_this() const _NOEXCEPT
        { return __weak_this_; }

    template <class _Up> friend class shared_ptr;
};

看完不禁有一个问题,weak_ptr 到底是什么时候被初始化的?

答案存在于 shared_ptr 的构造函数中:

template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
    : public __shared_ptr_access<_Tp, _Lp>
    {
		
        template<typename _Yp, typename = _SafeConv<_Yp>>
        explicit
        __shared_ptr(_Yp* __p)
        : _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
        {
            _M_enable_shared_from_this_with(__p);
        }
                
        template<typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
            typename enable_if<__has_esft_base<_Yp2>::value>::type
                _M_enable_shared_from_this_with(_Yp* __p) noexcept
            {
                if (auto __base = __enable_shared_from_this_base(_M_refcount, __p))
                    __base->_M_weak_assign(const_cast<_Yp2*>(__p), _M_refcount);
            }
        
        element_type*	   _M_ptr;         // Contained pointer.
        __shared_count<_Lp>  _M_refcount;    // Reference counter.
    }

__enable_shared_from_this_base_M_weak_assign 具体的实现看不到,我的理解是:

  1. 我们有继承了 enable_shared_from_this 的类对象的指针 __p
  2. __p.weak_from_this 获取 _weak_this 指针
  3. 通过 __p_M_refcount 来构造 weak_ptr 的控制块即可。

enable_shared_from_this 引入

enable_shared_from_this 中的 _weak_this 如何初始化?

CRTP 奇异递归模板方法

子类继承父类,父类的模板参数是子类对象。

https://zhuanlan.zhihu.com/p/54945314