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++网络库
posted @ 2022-06-08 20:38  路过的摸鱼侠  阅读(1237)  评论(0编辑  收藏  举报