1

C++ 智能指针浅析 - 路过的摸鱼侠

 1 year ago
source link: https://www.cnblogs.com/ljx-null/p/16357131.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 简介

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 的实现有关;
  • 其他数据,比如自定义删除器。

1099671-20220608203549742-2058889857.svg

模拟隐式转换行为

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

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>

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

1099671-20220608203653473-126159240.svg

当创建 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。

1099671-20220608203711797-2052659293.svg

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 列表中删除。

1099671-20220608203615236-1048593233.svg

解决循环引用

1099671-20220608203605828-1967138359.svg

在上图中,如果 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__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK