智能指针

循环引用

Cf.

  1. M.7 — std::shared_ptr
  2. M.8 — Circular dependency issues with std::shared_ptr, and std::weak_ptr
 
#include <memory>
#include <stdio.h>
 
#define PRINT_FUNC do { printf("%s called.\n", __FUNCTION__); }while(0);
 
using std::shared_ptr;
using std::weak_ptr;
 
struct B;
 
struct A
{
    A() { PRINT_FUNC }
    ~A() { PRINT_FUNC }
    shared_ptr<B> B_ptr;
};
 
struct B
{
    B() { PRINT_FUNC }
    ~B() {PRINT_FUNC}
    shared_ptr<A> A_ptr; // change to weak_ptr
};
 
 
int main()
{
    auto a_ptr = std::make_shared<A>();  // a对象
    auto b_ptr = std::make_shared<B>();  // b对象
    a_ptr->B_ptr = b_ptr;
    b_ptr->A_ptr = a_ptr;
    return 0;
}

main函数结束时,b_ptr先销毁1,检查引用计数发现a.B_ptr引用了同一个资源(b,此时引用计数为2),将自身引用解掉(此时引用计数减1),由于引用计数尚未减到0,因此,堆上b对象不会销毁。

接着,a_ptr销毁,检查引用计数,发现b.A_ptr引用了同一个资源(a,此时引用计数为2),将自身引用解掉(此时内部的引用计数为1),由于引用计数尚未减到0,因此,堆上a对象不会销毁。

整体看来,a持有b,b持有a,互相阻止了自己的释放。

如何解决?

将A、B中至少一个shared_ptr改为weak_ptr(弱引用)。

shared_ptr<T> sptr = xxx;
weak_ptr<T> wptr = sptr;  // 不会增加sptr内部的引用计数,弱引用

注意,如果仅将B中的shared_ptr改为weak_ptr,b对象必然会在a对象之后销毁。因为b对象销毁时,如果a对象未销毁,由于a.B_ptr是shared_ptr,会持有b对象,导致b对象的引用计数无法减为0,因此无法销毁。

std::shared_ptr

Tip

优先使用std::make_shared构造sthared_ptr. unique_ptr也有同样的规则。

写法更简洁

auto sptr(std::make_shared<YourClass>());
std::shared_ptr<YourClass> sptr2(new YourClass());

提升异常安全

假设有函数:

void foo(std::shared_ptr<A> spa, int priority);

用户调用如下:

foo(std::shared_ptr<A>(new A()), compute_priority());

在foo的函数体执行之前需要做三件事:

  1. new A 分配A对象
  2. 构造shared_ptr管理对象
  3. 执行compute_priority函数

编译器只能保证1在2之前执行,如果执行顺序是1-3-2,且compute_priority抛了未捕获的异常,会导致A对象无法释放。

也就是说,在你new一个对象和将它交给shared_ptr管理中间,可能会发生一些事情,导致后面的逻辑走不到。而使用std::make_shared会在分配对象之后立刻交给shared_ptr管理。

review

T sp = std::shared_ptr<T>(new T()); // #1
T sp = std::make_shared<T>(); // #2
  1. make_shared只分配一次内存(分配时考虑T对象的大小和shared_ptr控制块的大小)。而第一种写法实际上是两个阶段,第一个阶段是分配内存,构造T对象。第二个阶段是把构造好的对象指针传给std::shared_ptr的构造函数(此时会触发控制块的内存分配),交由shared_ptr管理。因此,如果有weak_ptr引用了这个对象,会导致shared_ptr的控制块无法释放(因为要给weak_ptr提供信息),如果只申请一次内存,将会附带T对象的内存也暂时无法释放2。而使用new就没这个问题。
  2. make_shared只支持public构造函数
  3. make_shared异常安全性更好,考虑如下两个调用,
    foo(std::shared_ptr(new int(3)), bar()); // #1
    foo(std::make_shared<int>(3), bar()); // #2
    由于函数参数的求值顺序是不确定的,调用#1包含了三个部分:分配内存,构造shared_ptr,调用bar. 如果求值顺序是先分配内存,再调用bar,此时抛了异常,那之前分配的内存就泄漏了。而调用#2只包含两个部分:调用make_shared (这一步会完成内存分配和构造shared_ptr)和调用bar. 问题的关键在于构造完对象之后有没有立刻把原始指针交给shared_ptr,中间会不会发生其他事情。

更多区别参考cppreference

Footnotes

  1. 总体遵循后创建先销毁的原则。

  2. https://stackoverflow.com/a/20895705