this 是 nullptr_C++智能指针3:内存布局(非侵入式、enable_shared_from_this & 侵入式)...

前两篇文章都关于智能指针。第一篇由Observer模式切入,主要讨论了std::weak_ptr的比较操作以及避免std::shared_ptr环形引用,提及了std::enable_shared_from_this,主要集中在应用方面。第二篇由多态切入,从MSVC的实现剖析std::shared_ptr内部指针所指涉的静态类型以解释析构行为。然而还有一些关键问题没有澄清,比如智能指针的内存布局。就如学 C++ 要学习Object Model,对于标准库或工作中使用的第三方库,应努力了解工具的内存布局,才能更好地在编码设计中做取舍。

对于标准库部分,本文讨论对象仍为std::shared_ptr,主要根据MSVC的实现剖析四种构造方式下的内存布局。此外,基于boost::intrusive_ptr补充侵入式智能指针的实现。更好的例子请参考llvm::IntrusiveRefCntPtr

  • 基本构造std::shared_ptr<T>(raw_pointer)

由于裸指针已经指涉了一块内存,没办法挨着这块内存分配Control Block,故ObjectControl Block分离。

  • 含自定义析构器 std::shared_ptr<T>(raw_pointer, deleter)

deleter会纳入Control Block。对于std::shared_ptr,析构器的类型不是该类型的组成部分。

  • 使用std::make_shared<T>(construct_parameters)

相比基本构造,由于没有预先分配内存,这种构造可以分配单块内存,将ObjectControl Block捆绑在一起。

  • 父类为std::enable_shared_from_this<DerivedClass>

这种定义类的方式使用了 CRTP。由于标准库的std::shared_ptr采用非侵入式设计,为了弥补引用计数与托管对象分离,导致无法在类中安全获取指涉到自身的std::shared_ptr,库补充了std::enable_shared_from_this组件用于侵入对象解决此问题。

  • boost::intrusive_ptr

侵入式设计,把Control Block塞入Object中共存亡,避免出现两个Control Block管理一个Object的情况。

展示的源码大多经过 SFINAE 筛选并实例化,内存布局会辅以图片。对模板推导的细节感兴趣可参考上一篇文章。基础知识可参考《Modern Effective C++》(条款19与21),尤其是这些构造法的适用场景。

[1] 基本构造

案例

#include <memory>

class Demo {};

int main()
{
    auto pDemo = std::shared_ptr<Demo>(new Demo);

    return 0;
}

分析:

template <>
class _Ptr_base<Demo> {
public:
    // ...

protected:
    constexpr _Ptr_base() noexcept = default;
    ~_Ptr_base() = default;

private:
    Demo* _Ptr{ nullptr };
    _Ref_count_base* _Rep{ nullptr };
};


template <>
class shared_ptr<Demo> : public _Ptr_base<Demo> {
public:
    explicit shared_ptr(Demo* _Px) {
        _Set_ptr_rep_and_enable_shared(_Px, new _Ref_count<_Ux>(_Px));
    }
}

对于std::shared_ptr<Demo>(new Demo),在构造函数体执行之前,首先是new,然后创建父类std::_Ptr_base<Demo>,最后是子类std::shared_ptr<Demo>

807398ecdd1a93e87cf9a73eaeeec55e.png

接着是在构造函数内的实参:

new _Ref_count<_Ux>(_Px);

class _Ref_count_base {
public:
    // ...

protected:
    constexpr _Ref_count_base() noexcept = default;

private:
    unsigned long _Uses = 1;
    unsigned long _Weaks = 1;
}


template <>
class _Ref_count<Demo> : public _Ref_count_base {
public:
    explicit _Ref_count(Demo* _Px) : _Ref_count_base(), _Ptr(_Px) {}

private:
    Demo* _Ptr;
};

先是父类std::_Ref_count_base,含记录std::shared_ptr的引用计数_Uses以及记录std::weak_ptr的引用计数_Weaks。子类std::_Ref_count<Demo>还有自己的数据_Ptr

bdb18533be896d156ab873fb0b121672.png

现在没有std::weak_ptr,为什么将_Weaks初始化为1?注意这里,_Uses负责Demo Instance的引用计数。而_Weaks负责std::_Ref_count<Demo> Instance的引用计数。以后的std::weak_ptr会在此基础上增加。减为 0 时std::_Ref_count<Demo> Instance被销毁,而这总是发生在_Uses减少为 0 后,否则会发生内存泄漏。

void shared_ptr<Demo>::_Set_ptr_rep_and_enable_shared(Demo* const _Px, _Ref_count_base* const _Rx) noexcept {
    this->_Set_ptr_rep(_Px, _Rx);
}

void _Ptr_base<Demo>::_Set_ptr_rep(Demo* _Other_ptr, _Ref_count_base* _Other_rep) noexcept {
    _Ptr = _Other_ptr;
    _Rep = _Other_rep;
}

这里设置数据成员。

813eed64872ce065f6489ae92607d849.png

图中有两个指针指向实例对象。std::shared_ptr中的主要服务于各种operator,可以方便拿到裸指针。而std::_Ref_count负责管理实例的生命周期,在销毁时时使用。析构细节可参考上一篇文章。

简化一下,其实就是:

ecdd33bad542b8060747ac51c8f2fb0d.png

读过《Effective Modern C++》肯定觉得似曾相识,这是最基本的std::shared_ptr的内存分布。所谓避免多个std::shared_ptr管理同一个对象以防止多次释放同一内存,意思是任何时候只能有一个Control Block来管理object的生命周期。而这也是std::enable_shared_from_this所解决的主要问题,如果在类内std::shared_ptr<T>(this)这样的语句,则会创建新的Control Block。必须继承std::enable_shared_from_this,使用shared_from_this()来安全地获取,这样不会创建新的Control Block

[2] 含自定义析构器

案例

#include <memory>

class Demo {};

int main()
{
    auto pDemoWithDeleter = std::shared_ptr<Demo>(new Demo, [](Demo* p) { delete p; });

    return 0;
}

分析:

template <>
class shared_ptr<Demo> : public _Ptr_base<Demo> {
    shared_ptr(Demo* _Px, LambdaExpr _Dt) {
        _Setpd(_Px, std::move(_Dt));
    }
};

void shared_ptr<Demo>::_Setpd(const Demo* _Px, LambdaExpr _Dt)
    _Set_ptr_rep_and_enable_shared(
        _Px, new _Ref_count_resource<Demo*, LambdaExpr>(_Px, std::move(_Dt)));
}

template <>
class _Ref_count_resource<Demo*, LambdaExpr> : public _Ref_count_base {
public:
    _Ref_count_resource(Demo* _Px, LambdaExpr _Dt)
        : _Ref_count_base(), _Mypair(_One_then_variadic_args_t(), std::move(_Dt), _Px) {}

private:
    _Compressed_pair<LambdaExpr, Demo*> _Mypair;
};

与普通构造的区别就是,std::_Ref_count<Demo>::_Ptr换成了std::_Ref_count_resource<Demo*, LambdaExpr>::_Mypair

std::_Compressed_pair是个很有意思的类,这里给出的代码完全保留了原模板定义,可以仔细分析:

template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && !is_final_v<_Ty1>>
class _Compressed_pair final : private _Ty1 { // store a pair of values, deriving from empty first
public:
    _Ty2 _Myval2;

    using _Mybase = _Ty1; // for visualization

    template <class... _Other2>
    constexpr explicit _Compressed_pair(_Zero_then_variadic_args_t, _Other2&&... _Val2)
        : _Ty1(), _Myval2(_STD forward<_Other2>(_Val2)...) {}

    template <class _Other1, class... _Other2>
    _Compressed_pair(_One_then_variadic_args_t, _Other1&& _Val1, _Other2&&... _Val2)
        : _Ty1(_STD forward<_Other1>(_Val1)), _Myval2(_STD forward<_Other2>(_Val2)...) {}

    _Ty1& _Get_first() noexcept {
        return *this;
    }

    const _Ty1& _Get_first() const noexcept {
        return *this;
    }
};

这个类的构造函数在初始化列表中先构造父类,然后初始化自己的数据成员。父类是个LambdaExpr类。这个类里面有数据成员吗?看看案例中自定义的析构器[](Demo* p) { delete p; }[]中没有绑定任何东西,所以该类显然没有数据成员。只要是有良心的 C++ 教材,应该会告诉初学者LambdaExpr其实是个can call function object,相当于类中有个operator(),若不以&=赋予LambdaExpr状态的话,这个类是个空类,在MSVCsizeof(LamdaExpr) == 1。而子类含一个数据,即Demo*,尺寸与机器有关。

使用_Get_first()方法可返回父类对应的can call function object以便调用。

验证:

#include <memory>
#include <iostream>

int main()
{
    auto pDemo = new Demo;
    auto deleter = [](Demo* p) {delete p; };

    std::_Compressed_pair<decltype(deleter), Demo*> pair(std::_One_then_variadic_args_t(), deleter, pDemo);

    std::cout << sizeof(deleter) << 'n';
    std::cout << sizeof(pair) << 'n';
    std::cout << sizeof(pair._Myval2) << 'n';

    pair._Get_first()(pDemo);

    return 0;
}

x64下输出为

1
8
8

所以说是个压缩的pair,当然这只是一个特例化,注意看std::_Compressed_pair的第三个模板参数,是做标签指派的,如果编译时值为false,那这个压缩的pair就名不副实了,就会成为普通意义上的pair

插曲结束,内存分布图为:

5757faae49c1ea17091fa6572c88b92d.png

[3] 使用 std::make_shared

案例

#include <memory>

class Demo {};

int main()
{
    auto pDemoUseMakeShared = std::make_shared<Demo>();

    return 0;
}

分析

inline shared_ptr<Demo> make_shared() {
    const auto _Rx = new _Ref_count_obj<Demo>();

    shared_ptr<Demo> _Ret;
    _Ret._Set_ptr_rep_and_enable_shared(_Rx->_Getptr(), _Rx);
    return _Ret;
}

template <>
class _Ref_count_obj<Demo> : public _Ref_count_base {
public:
    explicit _Ref_count_obj() : _Ref_count_base() {
        ::new (static_cast<void*>(&_Storage)) Demo();
    }

private:
    aligned_union_t<1, Demo> _Storage;
};

这里有个std::_Ref_count_obj<Demo>::_Storage,这个深究比较复杂,暂时忽略,总之可看成一块内存。构造时用了placement new,把实例塞进自己内部。这样一来可以清楚看到与pDemo的区别。也是所谓被托管的对象与引用计数绑定的实现。而使用std::shared_ptr构造则是两者分离的实现。构造函数体内的后续流程与基本构造就一致了,不再赘述。

149ba1118f99b04c6e035e3c2a4edaf3.png

这种构造不支持捆绑自定义的析构器。

[4] 父类为 std::enable_shared_from_this

案例

#include <memory>

class Demo : public std::enable_shared_from_this<Demo> {};

int main()
{
    auto pDemoWithEnableShared = std::shared_ptr<Demo>(new Demo);

    return 0;
}

由于多了父类,基础构造的new时要先构造Demo的父类,也就是std::enable_shared_from_this<Demo>

template <>
class enable_shared_from_this<Demo> {
public:
    // ...

protected:
    constexpr enable_shared_from_this() noexcept : _Wptr() {}

    mutable weak_ptr<Demo> _Wptr;
};

template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> {
public:
    constexpr weak_ptr() noexcept {}
}

注意到std::weak_ptr也派生自std::_Ptr_base,故类中会保存父类的两个指针数据成员。所以在std::shared_ptr构造函数体执行之前的内存布局如下:

5e0a3ed5605117fb4d482c7859d5c6a5.png

使用std::enable_shared_from_this有一点,就是不要在子类Demo的构造函数中调用shared_from_this()

shared_ptr<Demo> shared_ptr<Demo>::shared_from_this() { // return shared_ptr
    return shared_ptr<Demo>(_Wptr);
}

观察内存布局图就一目了然。这时_Wptr里都是空指针。使用一个空的std::weak_ptr构造std::shared_ptr马上抛异常。

接下来:

template <>
class shared_ptr<Demo> : public _Ptr_base<Demo> {
public:
    explicit shared_ptr(Demo* _Px) {
        _Set_ptr_rep_and_enable_shared(_Px, new _Ref_count<_Ux>(_Px));
    }
}

// 由于继承自 std::enable_shared_from_this<Demo>,源码中的 if constexpr 满足条件,故多了设置 _Wptr的代码
void shared_ptr<Demo>::_Set_ptr_rep_and_enable_shared(Demo* const _Px, _Ref_count_base* const _Rx) noexcept {
    this->_Set_ptr_rep(_Px, _Rx);
    if (_Px && _Px->_Wptr.expired()) {
        _Px->_Wptr = shared_ptr<Demo>(*this, _Px));
    }
}

this->_Set_ptr_rep(_Px, _Rx)设置std::shared_ptr中的两个父类成员指向正确的对象。

_Px->_Wptr = shared_ptr<Demo>(*this, _Px))这行代码发生了很多事情,出于异常安全考虑, 首先构造一个临时std::shared_ptr的构造,然后是std::weak_ptrmove assignment(operator=),最后是临时std::shared_ptr的析构。

shared_ptr<Demo>(*this, _Px))里没有new _Ref_count<Demo>(_Px)这样的语句,所以不会创建新的Control Block。而错误的用法std::shared_ptr<Demo>(this)则会。可见,不同的构造函数行为完全不同。

无须跟踪源码可知,首先_Uses一增一减没有发生变化,其次std::shared_ptr将自己父类中的_Ptr_Rep转交给std::weak_ptr,而原_Wptr中两者均为空指针,交换后可以直接丢弃(当然_Rep是被安全析构的,只不过是delete nullptr)即可。由于增加了一个std::weak_ptr来管理Control Block,故_Weaks + 1 为 2。

还是出于异常安全考虑,源码在std::weak_ptrmove assignment(operator=)中还构造了个临时的std::weak_ptr实施std::swap,不过这对最终结果没有影响。

main()函数开始一直到里面的object中的_Wptr被赋值结束,构造完成。整个过程只有一次new _Ref_count<T>,只有一个Control Block

因此最终版本为:

8996bf65a35985ba9df75ef58bd9fb59.png

简化后为:

b576a41b84437c7d584f02d6e1fcd663.png

为什么使用shared_from_this()可以安全地获取?只要不是从裸指针构造std::shared_ptr就不会构造Control Blcok。多了std::shared_ptr就会使_Uses自增 1 为 2。两个引用计数的自增操作均为原子操作。

#include <memory>

class Demo : public std::enable_shared_from_this<Demo> {};

int main() {
    auto pDemoWithEnableShared = std::shared_ptr<Demo>(new Demo);

    auto pDemoSharedFromOwn = pDemoWithEnableShared->shared_from_this();

    return 0;
}

布局:

3eef8ad26db1ac27d93041edca87c0d9.png

没有增加Control Blcok,内存管理正常。

可不可以不继承std::enable_shared_from_this,又避免创建两个Control Block?那只能在一个地方使用裸指针构造,比如在类内std::shared_ptr<Demo>(this)。那类外势必得使用这个返回的std::shared_ptr创建新的std::shared_ptr,但是这样已经不是原来的设计意图了,在类内的这个工作没有任何价值,结果就是显式创建两个std::shared_ptr共享一个实例而已。所以为了优雅地共享以达到设计目的,必须继承std::enable_shared_from_this

这其实是一种侵入式的设计,即用户必须在自定义类中安插成员(函数或数据)以完成设计者的意图,而继承某个定义好的基类是安插成员的经典方法。

至此,标准库提供的组件介绍完毕。接下来逛逛预备部队boost

[5] boost::intrusive_ptr

template<class T> 
class intrusive_ptr {
public:
    intrusive_ptr(T* p, bool add_ref = true) : px(p) {
        if (px != 0 && add_ref) {
            intrusive_ptr_add_ref(px);
        }
    }

    ~intrusive_ptr() {
        if (px != 0) {
            intrusive_ptr_release(px);
        }
    }

private:
    T * px;
}

这个智能指针可谓非常懒惰了,构造与析构分别委托给两个函数。而这两个函数由用户提供。用户需定义一个父类作为中间类,简单含引用计数即可。两个函数声明为友元。

template<class T>
class intrusive_ptr_base {
public:
    intrusive_ptr_base() : ref_count(0) {}

    friend void intrusive_ptr_add_ref(intrusive_ptr_base<T> const* p) {
        ++p->ref_count;
    }

    friend void intrusive_ptr_release(intrusive_ptr_base<T> const* p) {
        if (--p->ref_count == 0) {
            boost::checked_delete(static_cast<T const*>(s));
        }
    }

    boost::intrusive_ptr<T> self() {
        return boost::intrusive_ptr<T>((T*)this);
    }

private:
    mutable boost::detail::atomic_count ref_count;
};

所以boost::intrusive_ptr构造时做的唯一一件事就是增加引用计数,析构时减少引用计数,减为 0 时销毁对象。

最后实际工作的类派生于intrusive_ptr_base即可。由于Object自己管理自己的生命周期,所以可以随意使用boost::intrusive_ptr<T>(raw_pointer),包括boost::intrusive_ptr<T>(this)

内存布局:

e3d50872e7eef82da3f4469dd232203f.png

智能指针是个有趣的话题,在 C++ 看来是活用 RAII 来托管对象的生命周期。这些智能指针都依托于引用计数,其实是内存管理与垃圾回收的一种工具。更多 GC 细节,在其他高级语言如 Java、C# 等是热门话题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值