shared_ptr的删除器默认为delete操作符,但是由于shared_ptr指向的对象可能与控制块指向的对象不一致,所以删除器删除的对象可能与预期的不一致。理解智能指针的内存模型后,对理解这些行为很有帮助。

参考文章

本文参考了如下文章:

  1. shared_ptr
  2. shared_ptr - basics and internals with examples
  3. Aliasing constructed shared_ptr as key of map or set
  4. pointer_cast

shared指针行为

shared_ptr内存布局

如下图是std::shared_ptr指针的内存layout:

Alt text

shared_ptr的删除器是通过构造函数的方式传入的,不是shared_ptr智能指针类型的一部分。所以不同删除器的管理相同类型的智能指针可以放在同一个容器中(因为管理对象有相同的类型)。如果不提供删除器,则使用默认的delete操作符删除对象。

// file: /usr/include/c++/11/bits/shared_ptr_base.h
  // Counted ptr with no deleter or allocator support
  template<typename _Ptr, _Lock_policy _Lp>
    class _Sp_counted_ptr final : public _Sp_counted_base<_Lp>
    {
...
      virtual void
      _M_dispose() noexcept
      { delete _M_ptr; } // 默认直接调用delete
...
      virtual void*
      _M_get_deleter(const std::type_info&) noexcept
      { return nullptr; } // deleter 返回空指针
...
    private:
      _Ptr             _M_ptr;
    };

    ...

  // Support for custom deleter and/or allocator
  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>
      {
...
	_Deleter& _M_del() noexcept { return _Del_base::_S_get(*this); }
	_Alloc& _M_alloc() noexcept { return _Alloc_base::_S_get(*this); }
	_Ptr _M_ptr;
      };
...
      virtual void
      _M_dispose() noexcept
      { _M_impl._M_del()(_M_impl._M_ptr); } // 先获取deleter,然后将指针传入
...
    private:
      _Impl _M_impl;
    };

下面这个例子说明了deleter的行为:

#include <memory>
#include <iostream>
#include <vector>

struct SampleType {
    SampleType() {
        std::cout << "Constructor called\n";
    }

    ~SampleType() {
        std::cout << "Destructor called\n";
    }
};

int main() {
    std::shared_ptr<SampleType> ptr(new SampleType(), [](SampleType *p) {
        std::cout << "Custom deleter called\n";
        delete p;
    });
    std::shared_ptr<SampleType> normal_ptr(new SampleType());

    std::vector<std::shared_ptr<SampleType>> vec;
    vec.push_back(ptr);
    vec.push_back(normal_ptr);

    // ptr and normal_ptr are of the same type
    std::cout << "total:" << vec.size() << std::endl;
    return 0;
}

输出:

Constructor called
Constructor called
total:2
Destructor called
Custom deleter cal

注意:std::unique_ptrdeleter是模板参数,是其类型的一部分

指针位置相同,类型不同

这种情况对应的是图中的shared_ptr<B> three,下面是一个例子说明其行为:

#include <iostream>
#include <memory>

struct BaseType {
    BaseType() {
        std::cout << "BaseType constructor\n";
    }
    virtual ~BaseType() {
        std::cout << "BaseType destructor\n";
    }
};

struct DerivedType : public BaseType {
    DerivedType() {
        std::cout << "DerivedType constructor\n";
    }
    ~DerivedType() override {
        std::cout << "DerivedType destructor\n";
    }
};

int main() {
    // 尽管ptr是std::shared_ptr<BaseType>类型,但是因为control block指向的是DerivedType对象,所以DerivedType的析构函数会被调用
    std::shared_ptr<BaseType> ptr = std::make_shared<DerivedType>();
}

输出:

BaseType constructor
DerivedType constructor
DerivedType destructor
BaseType destructor

aliasing constructor

对应图中shared_ptr<C> four的情况,这种情况下其指向的对象不会在智能指针析构时释放资源。

#include <memory>
#include <iostream>

struct A {
    A() {
        std::cout << "A constructor\n";
    }

    ~A() {
        std::cout << "A destructor\n";
    }
};

struct B {
    B() {
        std::cout << "B constructor\n";
    }

    ~B() {
        std::cout << "B destructor\n";
    }
};

struct C {
    C() {
        std::cout << "C constructor\n";
    }

    ~C() {
        std::cout << "C destructor\n";
    }
    A a;
    B b;
};

int main() {
    std::shared_ptr<A> a_ptr;
    {
        std::shared_ptr<C> ptr = std::make_shared<C>();
        // a_ptr shared ref count with ptr, but point to ptr.a
        a_ptr = std::shared_ptr<A>{ptr, &ptr->a};
    }
    // ptr is out of scope, but object it points to is not destructed yet,since a_ptr still holds a reference to ptr.a
    std::cout << "C's construct should not be destructed yet\n";
}

输出:

A constructor
B constructor
C constructor
C's construct should not be destructed yet
C destructor
B destructor
A destructor

get()的返回值类型

  • shared_ptr<A>get()返回值类型是A的指针类型,不一定是Control Block指向的地址
  • 智能指针的共享所有权与其存储的具体的指向的类型与地址,是独立的,虽然大部分情况下它们是一样的

Cast操作

有了上面的理解基础,即智能指针的共享对象和其存储的具体对象类型和地址是独立的,就很自然的会碰到如下情况:

  • 想共享Control Block,但是希望存储的指针被当做另外一个类型

std::static_pointer_cast,std::dynamic_pointer_cast,std::const_pointer_cast,std::reinterpret_pointer_cast 用户实现以上操作,其各自的用法与对应的std::static_cast,std::dynamic_cast,std::const_cast,std::reinterpret_cast 相同,只不过这里是对shared_ptr类型get()返回的类型进行操作。

https://en.cppreference.com/w/cpp/memory/shared_ptr/pointer_cast

// static_pointer_cast
template<class T, class U>
std::shared_ptr<T> static_pointer_cast(const std::shared_ptr<U>& r) noexcept
{
    auto p = static_cast<typename std::shared_ptr<T>::element_type*>(r.get());
    return std::shared_ptr<T>{r, p};
}
// dynamic_pointer_cast
template<class T, class U>
std::shared_ptr<T> dynamic_pointer_cast(const std::shared_ptr<U>& r) noexcept
{
    if (auto p = dynamic_cast<typename std::shared_ptr<T>::element_type*>(r.get()))
        return std::shared_ptr<T>{r, p};
    else
        return std::shared_ptr<T>{};
}
// const_pointer_cast
template<class T, class U>
std::shared_ptr<T> const_pointer_cast(const std::shared_ptr<U>& r) noexcept
{
    auto p = const_cast<typename std::shared_ptr<T>::element_type*>(r.get());
    return std::shared_ptr<T>{r, p};
}
// reinterpret_pointer_cast
template<class T, class U>
std::shared_ptr<T> reinterpret_pointer_cast(const std::shared_ptr<U>& r) noexcept
{
    auto p = reinterpret_cast<typename std::shared_ptr<T>::element_type*>(r.get());
    return std::shared_ptr<T>{r, p};
}

unique指针行为

unique_ptr内存布局

Alt text

std::unique_ptr指针的删除器是其类型的一部分,这与std::shared_ptr是有区别的,并且其内存布局非常简单,没有共享控制块的原子操作。由于删除器是其类型的一部分,且内存模型不同,它和std::shared_ptr有很多区别,将在smartPtr构造&析构行为详细分析。