• C++ 智能指针浅析


    C++ 智能指针浅析

    为了解决 C++ 中内存管理这个老大难问题,C++ 11 中提供了三种可用的智能指针。(早期标准库中还存在一种 auto_ptr,但由于设计上的缺陷,已经被 unique_ptr 取代了)

    智能指针不仅能用来管理动态内存,还能用来管理其他类型的资源,比如互斥锁、数据库连接等,这种用资源管理对象来管理资源的思想被称为 RAII。

    原始指针的缺陷

    • 看不出来指向的是对象还是数组,也就不知道该用 delete 还是 delete[]
    • 不知道用完后是否应该销毁,也就是不包含所有权的信息;
    • 不知道该用 delete 还是其他方式释放该对象;
    • double free;
    • dangling pointers(悬空指针),即对象已经被释放了,但指针还指向它。

    shared_ptr

    shared_ptr 体现的是共享的所有权(shared ownership),通俗地讲,就是这个被管理的对象可以被多个用户使用,只有所有用户都不再需要该资源时,它才可以被释放。

    为了实现共享的所有权,shared_ptr 会维护一个引用计数,表示有多少 shared_ptr 指向这个被管理的对象。

    简单地来说,当 shared_ptr 被拷贝时(不管是我们主动调用,还是将它作为传值参数或返回值),引用计数就会增加;当 shared_ptr 被析构时(比如离开它的作用域),引用计数就会减少。当引用计数归零时,这个被管理的对象就会被释放。

    shared_ptr 中引用计数的操作是原子的,这点和 shared_ptr 的线程安全性有关。

    shared_ptr 使用规范

    要正确地使用 shared_ptr,需要注意一些问题:

    • 不使用相同的内置指针初始化或 reset 多个 shared_ptr,原因请看后文的 shared_ptr 实现,这样操作会产生多个控制块对象,从而造成 double free;
    • get() 返回的原始指针只应该有一种用途,用来作为参数传递给遗留接口;
      • 不应该 delete get() 返回的指针;
      • 不使用 get() 的返回值去初始化或 reset 另一个智能指针;
      • 当最后一个对应的智能指针被销毁时,get() 返回的指针就无效了;
    • 如果使用智能指针管理资源而不是内存,应当定义自定义删除器;
    • 如果将 shared_ptr 存放到容器中,之后不再需要其中的部分元素,应当调用 erase 将它们删除,不然这些元素的生命周期会被意外地延长。

    在单条语句里将 new 来的指针放到智能指针里

    int priority();
    
    void processWidget(std::shared_ptr<Widget> pw, int priority);
    
    processWidget(std::shared_ptr<Widget>(new Widget), priority());
    

    该调用事实上由三部分组成:

    1. new Widget
    2. 调用 shared_ptr 构造函数
    3. 调用 priority()

    1 应当发生在 2 之前,但标准并没有规定 3 要在何时发生;如果编译器以 1、3、2 这样的顺序来执行,那么如果调用 priority() 时抛出异常,那么就会出现内存泄漏。

    如果将程序改为:

    std::shared_ptr<Widget> pw(new Widget);
    
    processWidget(pw, priority());
    

    可以强制上面的过程以 1、2、3 的顺序发生,消除了内存泄漏的隐患。

    此外,将这里改成 std::move() 效率更高,因为移动不涉及引用计数的增减。

    std::shared_ptr<Widget> pw(new Widget);
    
    processWidget(std::move(pw), priority());
    

    避免该内存泄露问题的另一种方法是改用 make_shared()

    processWidget(std::make_shared<Widget>(), priority());
    

    make_shared

    使用 make_shared() 的优势:

    • 避免上面提到的内存泄露问题;
    • 效率更高:先 new 出内存再传递给 shared_ptr,涉及到两次内存分配,一次分配被管理的对象,一次分配控制块对象;但如果改用 make_shared(),只需要分配一次内存就足够容纳这两个对象。

    使用 make_shared() 的缺陷:

    • 不允许指定自定义删除器。
    • 没法用 {} 初始化,make_shared() 将参数完美转发给构造函数时,默认使用 ()
    • 如果类重载了 operator newoperator delete,它的内存分配行为可能表现出特殊的行为;
      • 比如 Widget 类的 operator newoperator delete 可能只处理大小为 sizeof(Widget) 的内存分配;
      • make_shared() 不只是分配对象的动态内存,还会同时分配控制块的内存,这种情况下,就会造成麻烦。
    • 由于控制块和对象放在同一块内存里,引用计数为 0 的时候,对象并不会被释放,只有等到控制块也不再需要的时候,这块内存才会被释放;
      • 只要还有 weak_ptr 指向这个对象,控制块就不会被释放;
      • 对于特别大的对象,这可能是问题。

    unique_ptr

    unique_ptr 体现的是独占/排他的所有权(exclusive ownership),通俗地讲,就是这个被管理的对象为单个用户所有,当该用户不再需要这个资源时,它就会被释放掉;同时,该用户还可以将资源的所有权移交给其他用户。

    unique_ptr 的拷贝构造和赋值是删除的,提供了移动构造和赋值,因此要想将持有的独占资源交给其他 unique_ptr,往往需要显式地调用 std::move() 来触发移动操作。

    UML 中的组合(或称复合,Composition)关系,就可以使用 unique_ptr 来建模,当整体被析构时,部分也会跟着析构。

    unique_ptr 可以转换为 shared_ptr,因此 unique_ptr 很适合作为工厂函数的返回值,因为你不知道使用者需要的所有权语义究竟是独占的还是共享的;相反,一旦所有权交给了 shared_ptr,就没法再转换为独占的所有权了。

    C++11 中没有类似于 make_shared() 的标准库函数,应当将 new 返回的指针传递给 unique_ptr 的构造函数;C++14 中加入了 make_unique()

    unique_ptr 的大小

    • 使用默认删除器的情况下,unique_ptr 内部只维护了一个指针,和原始指针大小相同

    由于 unique_ptr 直接将自定义删除器存储在对象内部

    • 使用函数指针作为删除器,大小为变为两倍的原始指针
    • 使用函数对象作为删除器
      • 无状态的函数对象,比如不捕获变量的 lambda 表达式,不影响 uniqur_ptr 的大小
      • 否则,随着函数对象的大小增加而增加

    为什么不捕获变量的 lambda 表达式在 unique_ptr 里不占空间?

    实现上,lambda 表达式其实是个匿名类对象,如果它不捕获任何变量,就是个空的类,C++ 规定任何对象和对象成员的大小都至少是 1,即使该对象的类型是一个空的类,这就能保证相同类型的不同对象的地址总是不同的。

    class Empty {}; 
    class HoldsAnInt {
    private: 
        int x; 
        Empty e;
    };
    

    由于上述规定的存在,导致 sizeof(HoldsAnInt) > sizeof(int)

    但该规定存在例外,那就是这个空的类作为基类的时候,是不需要占据空间的。这种优化被称为空基类优化(empty base optimization,EBO)。

    class HoldsAnInt: private Empty { 
    private: 
      int x;
    };
    

    这种情况下,sizeof(HoldsAnInt) == sizeof(int)

    我们查看 unique_ptr 的源码,会发现它是这样存储指针和删除器的:

    tuple<pointer, _Dp> _M_t;
    

    gcc 的 tuple 的实现上应用 EBO 做空间压缩,所以不捕获变量的 lambda 表达式作为删除器时,unique_ptr 的空间不会变大。

    对于程序员来说,用不捕获变量的 lambda 表达式做删除器是非常常见的情况,所以这是一个很有价值的优化。

    auto_ptr 的缺陷

    auto_ptr 是早期标准库中实现的一种智能指针,具有 unique_ptr 的部分特性,它用拷贝来模拟资源所有权的移交,拷贝 auto_ptr 会导致源 auto_ptr 被设为 null,这种行为是比较匪夷所思的。

    std::auto_ptr<Invesetment> pInv2(pInv1);    // pInv1 被设为 null
    pInv1 = pInv2;    // pInv2 被设为 null
    

    假设将 auto_ptr 存储到容器里,并使用泛型算法对容器做操作,由于泛型算法可能将元素拷贝到局部临时对象中,会导致操作后的容器里存在大量空的 auto_ptr,因此 auto_ptr 不能和容器一起使用。

    和 unique_ptr 作比较,unique_ptr 仅允许移动操作,而禁止了拷贝操作,语义更加清晰且安全。

    自定义删除器

    shared_ptr 和 unique_ptr 都允许自定义删除器,区别在于 unique_ptr 的自定义删除器是模板参数的一部分,而 shared_ptr 仅仅是作为构造函数的一个参数。

    auto loggingDel = [](Widget *pw)        // 自定义删除器
                    {
                        makeLogEntry(pw);
                        delete pw;
                    };
    
    // 删除器类型是指针类型的一部分
    std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
    
    // 删除器类型不是指针类型的一部分
    std::shared_ptr<Widget> spw(new Widget, loggingDel);
    

    不包含删除器类型模板参数给了 shared_ptr 更大的灵活性,比如 std::shared_ptr<Widget> 的容器中可以放删除器不同的各种 Widget 对象。

    shared_ptr 的大小不会随着自定义删除器的大小而增加,因为这部分是放在它的控制块对象里;但 uniqur_ptr 是直接将删除器存到指针对象内了,因此它的大小是会随着自定义删除器的大小变化的。

    智能指针和动态数组

    shared_ptr 和动态数组

    shared_ptr 不支持动态数组的管理,将动态数组传递给它后,它仍会使用 delete,而不是 delete[] 去释放这块内存,因此必须提供自己的删除器。

    可以直接使用 default_delete 类型作为自定义删除器,这个类提供了针对数组类型的偏特化,会调用 delete[] 来释放内存。

    std::shared_ptr<int> sp3(new int[10](), std::default_delete<int[]>());
    

    同时,由于 shared_ptr 没有提供下标运算符,要访问数组中的元素,必须通过 get() 获取内置指针,用它来访问数组元素。

    作为替代,可以使用 boost 提供的 boost::scoped_arrayboost::shared_array 来管理动态数组。

    C++17 的改进

    支持直接用数组作为模板参数,它会正确地调用 delete[],且增加了下标运算符:

    std::shared_ptr<int[]> sp3(new int[10]());
    int i = sp3[2];
    

    使用 unique_ptr 管理动态数组

    在类型名后面加 [],它会自动调用 delete[] 来释放内存,且可以使用下标运算符访问其中的元素。

    unique_ptr<int[]> int_array(new int[10]{ 1, 2, 3 });
    cout << int_array[1] << endl;
    

    但比起使用 unique_ptr,更应该优先考虑 std::arraystd::vectorstd::string 等数据容器。

    RAII

    RAII 简介

    Resource Acquisition Is Initialization (RAII),即获取到资源后立刻移交给资源管理对象。资源管理对象使用它们的析构函数确保资源被释放。这里说的资源可以是内存、数据库连接、互斥锁等各种形式的资源。

    假设有一个 C 风格 API 的 Mutex 类:

    void lock(Mutex *pm);
    void unlock(Mutex *pm);
    

    Mutex 的资源管理类(C++11 中类似的类叫 std::lock_guard)可以定义为:

    class LockGuard
    {
    public:
        explicit LockGuard(Mutex* pm) :mutexPtr(pm) { lock(mutexPtr); }
         ~LockGuard() { unlock(mutexPtr); }
    private:
        Mutex* mutexPtr;
    };
    

    对想要写出异常安全代码的程序员来说,RAII 非常重要,因为如果在代码中显式地释放资源,就可能出现因为异常抛出而资源没能正确释放地情况,比如下面这段代码,如果 new 抛异常,mutex 就不会被解锁。

    void PrettyMenu::changeBackground(std::istream& imgSrc)
    {
       lock(&mutex);
       delete bgImage;
       ++imageChanges;
       bgImage = new Image(imgSrc);
       unlock(&mutex);
    }
    

    但如果改为用 RAII 的方式来管理资源,如果有异常被抛出,栈展开的过程中,资源管理对象就会被析构掉,从而释放掉被管理的资源。

    void PrettyMenu::changeBackground(std::istream& imgSrc)
    {
        Lock ml(&mutex);
    
        delete bgImage;
        ++imageChanges;
        bgImage = new Image(imgSrc);
    }
    

    用智能指针实现 RAII

    C++11 后,可以通过智能指针来实现 RAII,对于共享资源,将它交给 shared_ptr,而对于独占资源,将它交给 unique_ptr,同时通过使用自定义删除器,来决定资源该怎样被释放。

    shared_ptr 的实现

    shared_ptr 实现上包含两个指针:一个指向被管理的对象,一个指向动态分配的控制块对象。控制块对象里包含:

    • 引用计数(use_count),用来判断是否可以释放被管理的对象;
    • 弱引用计数(weak_count),和 weak_ptr 的实现有关;
    • 其他数据,比如自定义删除器。

    模拟隐式转换行为

    智能指针可以模拟指针的隐式转换行为,自动将派生类指针转换为基类指针,为了做到这点,需要为智能指针增加一个泛化拷贝构造函数:

    template<typename T> 
    class SmartPtr { 
    public: 
        template<typename U>
        SmartPtr(const SmartPtr<U>& other): heldPtr(other.get()) 
        { 
            //... 
        }
        T* get() const { return heldPtr; }
        //...
        private:
        T *heldPtr;
    };
    

    这个泛化拷贝构造函数不是 explicit 的,以支持隐式转换;同时它的初始化列表告诉我们只有能隐式转换为 T* 类型的 U* 才能通过编译。

    源码分析

    下面包含一些 gcc 的源码分析,不想看代码的读者可以直接跳过本章。

    gcc 中控制块对象的类叫 _Sp_counted_base,包含两个引用计数,它们都是原子类型:

    template<_Lock_policy _Lp = __default_lock_policy>
    class _Sp_counted_base : public _Mutex_base<_Lp>
    {
        // ...
        _Atomic_word  _M_use_count;     // #shared
        _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
        // ... 
    };
    

    shared_ptr 中会保存指向 _Sp_counted_base 的指针。

    _Sp_counted_base<_Lp>*  _M_pi;
    

    之前我们说过 shared_ptr 的模板参数中是不包含自定义删除器类型的,这就导致 deleter 没法保存在 shared_ptr 内部。

    template< class T > class shared_ptr;   // shared_ptr 的声明
    
    template< class Y, class Deleter >
    shared_ptr( Y* ptr, Deleter d );    // 带自定义删除器的构造函数
    

    很显然,deleter 只能存放在控制块对象中。为此,_Sp_counted_base 有一个带删除器模板参数的派生类 _Sp_counted_deleter

    template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
    class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
    

    这些类之间的关系如下图所示:

    当创建 shared_ptr 时,会动态分配 _Sp_counted_deleter 对象,并将自定义删除器存到它里面。

    template<typename _Ptr, typename _Deleter, typename _Alloc,
    typename = typename __not_alloc_shared_tag<_Deleter>::type>
    __shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0)
    {
      typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type;
      __try
        {
            typename _Sp_cd_type::__allocator_type __a2(__a);
            auto __guard = std::__allocate_guarded(__a2);
            _Sp_cd_type* __mem = __guard.get();
            ::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));
            _M_pi = __mem;
            __guard = nullptr;
        }
        __catch(...)
        {
            __d(__p); // Call _Deleter on __p.
            __throw_exception_again;
        }
    }
    

    shared_ptr 析构的时候,_M_pi->_M_release() 会被调用,(原子地)减少引用计数:

    void _M_release() // nothrow
    {
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) {
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
        _M_dispose();
    
        if (_Mutex_base<_Lp>::_S_need_barriers) {
            __atomic_thread_fence(__ATOMIC_ACQ_REL);
        }
    
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
        if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) {
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
            _M_destroy();
        }
        }
    }
    

    如果 use_count 减少到 0,调用虚函数 _M_dispose(),其实调用到的是_Sp_counted_deleter 的实现,由此,自定义删除器被用来销毁被管理的对象。

    如果 weak_count 减少到 0,调用 _M_destroy(),销毁控制块对象。

    virtual void _M_dispose() noexcept
    { 
        _M_impl._M_del()(_M_impl._M_ptr); 
    }
    
    virtual void _M_destroy() noexcept
    {
        __allocator_type __a(_M_impl._M_alloc());
        __allocated_ptr<__allocator_type> __guard_ptr{ __a, this };
        this->~_Sp_counted_deleter();
    }
    

    在不定义自定义删除器的情况下,如果被管理对象是是非数组类型,_M_pi 指向_Sp_counted_deleter 的默认实现 _Sp_counted_ptr,它直接调 delete 析构被管理对象。

    virtual void _M_dispose() noexcept
    { 
        delete _M_ptr; 
    }
    
    virtual void _M_destroy() noexcept
    { 
        delete this; 
    }
    

    但如果被管理对象是是数组类型,自定义删除器将被设置为 __sp_array_delete,它调用 delete[] 来析构对象。

    struct __sp_array_delete
    {
        template<typename _Yp> void operator() (_Yp* __p) const { delete[] __p; }
    };
    

    shared_ptr 的性能分析

    • 大小是原始指针的两倍,因为它要同时持有指向被管理对象的指针和控制块对象的指针;
    • 引用计数的内存必须是动态分配的,可以用 make_shared 来对这点做优化;
    • 引用计数操作是原子的,原子操作比非原子操作要慢;
    • 控制块对象包含继承关系,为了正确的销毁对象使用了虚函数,因此对象销毁的时候要承担虚函数带来的开销。

    移动和引用计数

    用一个 shared_ptr 移动构造另一个 shared_ptr 不会导致引用计数的变化,新的 shared_ptr 会直接接管老的 shared_ptr 的控制块对象,因此 shared_ptr 的移动操作要比拷贝操作高效。

    weak_ptr

    weak_ptr 不负责对象的生命周期管理,它指向一个由 shared_ptr 管理的对象,但不改变它的引用计数(use_count)。通过 weak_ptr 可以判它所指向的对象是否已经被销毁。有两种方法可以做这种判断:

    • 尝试用 lock() 提升它为 shared_ptr,如果过期,返回的结果为空;
    • 用它构造一个新的 shared_ptr,如果过期,会抛 std::bad_weak_ptr 异常。
    auto spw2 = wpw.lock();
    std::shared_ptr<Widget> spw3(wpw);
    

    weak_ptr 的实现

    weak_ptr 的实现和 shared_ptr 的控制块对象相关。

    由于 weak_ptr 想要知道 shared_ptr 管理的对象是否已经被析构了,那么最直接的手段就是让它访问 shared_ptr 的控制块对象,查看 use_count 是否已经是 0。

    控制块对象的生命周期必须延续到所有 shared_ptr 和 weak_ptr 都已经析构掉,它才可以析构。所以需要一个控制块对象的引用计数来管理它的生命周期,即 weak_count。

    weak_ptr 的使用场景

    shared_ptr 可能悬空的情况下,都可以使用 weak_ptr 来做 shared_ptr 是否悬空的判断。比如 UML 中的聚合(aggregation)关系,一般由整体持有部分的 shared_ptr,部分持有整体的 weak_ptr,并使用升级操作,来判断整体是否还存活。

    带缓存的工厂函数

    假设说有一个开销很大的工厂函数,它针对不同的 ID 产生不同的产品,但该工厂函数的开销很大,我们希望能缓存下来它的产品:

    std::shared_ptr<const Widget> loadWidget(WidgetID id);
    

    我们希望当工厂函数客户端不再需要该缓存对象时,该对象可以被析构,因此我们用哈希表存储对象的 weak_ptr 来做缓存,获取对象时,先尝试升级缓存中的 weak_ptr,如果能成功,说明对象仍未被释放,可以节省调用工厂函数的开销:

    std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
    {
        static std::unordered_map<WidgetID,
                                  std::weak_ptr<const Widget>> cache;
                                    
        auto objPtr = cache[id].lock();    
    
        if (!objPtr) {                      
            objPtr = loadWidget(id);        
            cache[id] = objPtr;             
        }
        return objPtr;
    }
    

    观察者模式

    观察者模式允许我们实现一种“发布-订阅”机制,当对象的状态发生变化时,通知正在“观察”该对象的其他对象。

    观察者模式中有两种角色:subject 或 publisher 是状态会发生变化的对象;observer 或 subscriber 是在 subject 状态发生变化时要(通过调用 observer 的 update 接口)通知的对象。

    如果使用裸指针来保存 observer,有可能发生的是 observer 已经被析构,但 subject 还不知道,又调用了 observer 的 update 方法,导致未定义行为。

    解决方法是用 weak_ptr 保存 observer,在调用 update 前先升级为 shared_ptr,如果 observer 仍存活就调用 update,否则将它从 observer 列表中删除。

    解决循环引用

    在上图中,如果 B 也有指向 A 的引用,这时就不能选择 shared_ptr,因为如果有多个 shared_ptr 的引用关系形成环状,会导致这些指针的引用计数永远都不会归零,结果就是这些对象永远都不会被释放,即使程序中的其他部分已经不持有指向它们的智能指针。

    如果我们使用裸指针,就可能出现 A 已经被析构了,B 还尝试调用它的方法,导致未定义行为。

    解决方法:将引用链中的一个指针改为使用 weak_ptr 从而打破环状关系。

    shared_ptr 的线程安全性

    智能指针本身的线程安全

    如果多个线程在不同的 shared_ptr 对象(即使这些对象是拷贝出来的,共享同一个对象的所有权)上不加同步地调用任何成员函数(包括拷贝和赋值),都是线程安全的。

    如果多个线程不加同步地访问同一个 shared_ptr 对象,并且有线程使用了非 const 的成员函数(拷贝和赋值也是非 const 的),那么会发生数据竞争。虽然智能指针针对引用计数的操作是原子的,但是非 const 的成员函数往往涉及多个操作,这些操作组合在一起不是原子的。

    被管理对象的线程安全

    使用多个不同的 shared_ptr 对象访问同一个被管理对象,如果对该对象的操作本身不是线程安全的,那么即使通过智能指针访问,也会发生数据竞争,智能指针并不对被管理对象的线程安全性负责。

    enable_shared_from_this

    之前我们说过,当你持有一个 shared_ptr ,绝不应该再从它的原始指针(不管是 new 出来的还是通过 get() 得到的)中创建出一个新的 shared_ptr ,因为它们的引用计数是不共享的,会带来 double free 的问题。对于这点,即使是 this 指针也不例外。

    struct S
    {
      shared_ptr<S> dangerous()
      {
         return shared_ptr<S>(this);   // don't do this!
      }
    };
    
    int main()
    {
       shared_ptr<S> sp1(new S);
       shared_ptr<S> sp2 = sp1->dangerous();
       return 0;
    }
    

    在这个例子中,智能指针 sp1 和 sp2 指向的是同一个底层对象,但 sp1 和 sp2 对此一无所知,它们各自管理自己的引用计数,因此这块内存会被释放两次。

    要避免此问题,可以使用类模板 enable_shared_from_this,以它的派生类作为模板实参,这种继承的模式被称为 curiously recurring template pattern(CRTP)。

    struct S : enable_shared_from_this<S>
    {
      shared_ptr<S> not_dangerous()
      {
        return shared_from_this();
      }
    };
    
    int main()
    {
       shared_ptr<S> sp1(new S);
       shared_ptr<S> sp2 = sp1->not_dangerous();
       return 0;
    }
    

    使用场景

    有时候对象可能需要获得一个指向自己的 shared_ptr,以传递给 lambda 表达式或消息队列的时候,就需要调用 shared_from_this()

    boost::asio::async_write(socket_, boost::asio::buffer(message_),
          boost::bind(&tcp_connection::handle_write, shared_from_this()));
    

    上面这个例子来自 asio 的教程,asio 服务器会为每个客户端连接动态创建一个 tcp_connection 对象(包含连接的上下文信息),并在上面执行异步操作,所以必须保证 tcp_connection 对象存活到回调函数被调用的时候。

    但由于异步操作的特点,你不知道回调函数 handle_write 什么时候会被调用,所以很难直接去管理 tcp_connection 的生命周期,通常的做法是用 shared_ptr 来管理它,并且让异步操作捕获一份它的 shared_ptr(不管是通过 std::bind 还是 lambda 表达式的捕获列表),这样只要异步操作还未执行,tcp_connection 对象就不会被析构。

    如果在 tcp_connection 的成员函数中要执行异步操作,就必须捕获一个指向自己的 shared_ptr,这时候就需要调用 shared_from_this(),因此 tcp_connection 必须继承 enable_shared_from_this。

    使用限制

    必须通过 shared_ptr 来调用 shared_from_this(),下面的代码是错误的:

    int main()
    {
       S *p = new S;
       shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
    }
    

    此外,shared_from_this() 不能在构造函数里被调用。

    参考材料

    • [1] C++ Primer
    • [2] Effective Modern C++
    • [3] Effective C++
    • [4] Linux多线程服务端编程 使用muduo C++网络库

    __EOF__

  • 本文作者: 路过的摸鱼侠
  • 本文链接: https://www.cnblogs.com/ljx-null/p/16357131.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【大数据】Hadoop三大核心组件入门
    【面试】摸鱼快看:关于selenium/ui自动化的面试题
    08.23类属性和实例属性
    Windows驱动反调试的一种手段
    3. MongoDB高级进阶
    onps栈使用说明(3)——tcp、udp通讯测试
    Rxjava学习(一)简单分析Rxjava调用流程
    leetcode-151. 颠倒字符串中的单词-20220823
    如何申请外贸公司的邮箱
    C#,人工智能,深度学习,OpenCV开发,入门教程——Visual Studio 2022,OpenCvSharp环境搭建与可视化
  • 原文地址:https://www.cnblogs.com/ljx-null/p/16357131.html