智能指针内存模型
shared_ptr
的删除器默认为delete
操作符,但是由于shared_ptr
指向的对象可能与控制块指向的对象不一致,所以删除器删除的对象可能与预期的不一致。理解智能指针的内存模型后,对理解这些行为很有帮助。
参考文章
本文参考了如下文章:
- shared_ptr
- shared_ptr - basics and internals with examples
- Aliasing constructed shared_ptr as key of map or set
- pointer_cast
shared指针行为
shared_ptr内存布局
如下图是std::shared_ptr
指针的内存layout:
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_ptr
的deleter
是模板参数,是其类型的一部分
指针位置相同,类型不同
这种情况对应的是图中的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内存布局
std::unique_ptr
指针的删除器是其类型的一部分,这与std::shared_ptr
是有区别的,并且其内存布局非常简单,没有共享控制块的原子操作。由于删除器是其类型的一部分,且内存模型不同,它和std::shared_ptr
有很多区别,将在smartPtr构造&析构行为详细分析。