Skip to content

Latest commit

 

History

History
128 lines (94 loc) · 5.27 KB

File metadata and controls

128 lines (94 loc) · 5.27 KB

条款19:使用std::shared_ptr管理具备共享所有权的资源

引用计数的存在会带来一些性能影响:

  • std::shared_ptr 的大小是裸指针的两倍。 因为它们内部既包含一个指向资源的裸指针,也包含一个指向该资源的引用计数的裸指针(非标准规定)。
  • 引用计数的内存必须动态分配。 引用计数与被指向的对象相关联,然而被指向的对象对此却一无所知,因为它们没有存储引用计数的位置。
  • 引用计数的递增和递减必须是原子操作。 因为在不同线程中可能存在并发的读写器。原子操作一般都比非原子操作满,所以即使引用计数只有一个字长,也应当假设读写它们的成本是比较高昂的。

std::shared_ptr 也使用 delete 运算符作为其默认资源析构机制,它也支持自定义析构器。对于 std::unique_ptr 而言,析构器的类型是智能指针类型的一部分,但对于 std::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);

考虑两个 std::shared_ptr<widget> ,各有一个不同类型的自定义析构器,由于 pw1pw2 具有相同类型,所以可以被放置在元素类型相同的容器中:

auto customDeleter1 = [](Widget* pw) {...};
auto customDeleter2 = [](WIdget* pw) {...}:

std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);

std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };

std::shared_ptr 自定义析构器不会改变它的大小,无论析构器是怎样的类型,它的大小都相当于裸指针的两倍。

我们可以想想与 std::shared_ptr<T> 对象相关的内存模型:

指向T类型对象的指针 指向控制块的指针
T类型的对象 引用计数
弱计数
其他数据(自定义删除器、分配器等)

控制块的创建遵循以下规则:

  • std::make_shared 总是创建一个控制块。
  • 从具备专属所有权的指针(std::unique_ptr)出发构造一个 std::shared_ptr 时,会创建一个控制块。
  • std::shared_ptr 构造函数使用裸指针作为实参来调用时,它会创建一个控制块。

这会导致一个后果:使用同一个裸指针构不止一个 std::shared_ptr 会导致被指向的对象将会有多重控制块。意味着该对象会被析构多次。导致未定义行为。

auto pw = new Widget;
std::shared_ptr<Widget> spw1(pw, loggingDel);
std::shared_ptr<Widget> spw2(pw, loggingDel);   // dangerous!

如果必须将一个裸指针传递给 std::shared_ptr 构造函数,建议直接传递 new 的结果,而非裸指针变量:

std::shared_ptr<Widget> spw1(new Widget, loggingDel);

使用裸指针作为 std::shared_ptr 构造函数实参时,会有一种行为导致 this 指针的多重控制块。假设我们的程序使用 std::shared_ptr 托管 Widget 对象,并用数据结构追踪被处理的 Widget

std::vector<std::shared_ptr<Widget>> processedWidgets;

假设 Widget 有个成员用来做处理:

class Widget {
public:
    ...
    void process();
    ...
};

void Widget::process() {
    ...
    processedWidgets.emplace_back(this);    
    // dangerous!
}

由此构造的 std::shared_ptr 将为其所指向的 *this 对象创建一个新的控制块,如果该对象事先已经被 std::shared_ptr 拥有,这会导致多重控制块,造成未定义行为。

标准库提供了一种方法可以安全地由 this 指针创建一个 std::shared_ptr

class Widget: public std::enable_shared_from_this<Widget> {
public:
    ...
    void process();
    ...
};

void Widget::process() {
    ...
    processedWidgets.emplace_back(shared_from_this());
}

从内部实现角度看, shared_from_this 查询当前对象的控制块,并创建一个指向该控制块的新 std::shared_ptr。这样的是西安依赖于当前对象已有一个与其关联的控制块。为了是西安这一点,就必须有一个已经存在的指向当前对象的 std::shared_ptr。如果这样的 std::shared_ptr 不存在,那么该行为未定义,通常抛出异常。

为了避免上述问题,通常将此类的构造函数声明为private,只允许用户通过返回 std::shared_ptr 的工厂函数来创建对象:

class Widget: public std::enable_shared_from_this<Widget> {
public:
    template<typename.. Ts>
    static std::shared_ptr<Widget> create(Ts&&... params);
    ...
    void process();
    ...
private:
    ...   // 构造函数
};

std::shared_ptr 不能做的事包括数组处理:它并未提供 operator[] 接口,并且禁止 std::unique_ptr<T[]>std::shared_ptr 转换。且无法向 std::unique_ptr 转换。

此外,std::shared_ptr 支持从派生类到基类指针的转换。