如何通过srd::shared_ptr高效管理共享所有权的资源?

摘要:本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢! 博客已经迁移到 "这里啦" 使用带垃圾回
本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢! 博客已经迁移到这里啦 使用带垃圾回收机制语言的程序员指出并嘲笑C++程序员需要遭受防止资源泄漏的痛苦。“多么原始啊”他们嘲笑道,“20世纪60年代的Lisp留下的备忘录你还不记得了吗?机器(而不是人类)应该管理资源的生命周期”。C++开发人员转了转他们的眼睛,“你所说的备忘录是指,那些资源只有内存以及资源的回收时间不确定的时候吗?我们更喜欢比较普遍以及可预测的析构函数,谢谢你。”但是我们只是虚张声势而已。垃圾回收机制确实很方便,而且手动的生命周期管理确实看起来像:使用石刀和熊皮来构造一个记忆存储电路(意味着几乎不可能的任务,constructing a mnemonic memory circuit using stone knives and bear skins,出自星际迷途)。为什么我们不能同时拥有两个世界的精华部分呢:创造一个系统,这个系统能自动工作(比如垃圾回收机制),还能应用到所有资源上以及能拥有可预测的生命周期(比如析构函数)? C++11中是用std::shared_ptr把两个世界的优点绑定在一起的。通过std::shared_par可以访问对象,这个对象的生命周期由智能指针以共享所有权的语义来管理。没有一个明确的std::shared_ptr占有这个对象。取而代之的是,所有指向这个对象的std::shared_ptr一起合作来确保:当这个对象不再被需要的时候,它能被销毁。当最后一个指向对象的std::shared_ptr不再指向这个对象(比如,因为std::shared_ptr被销毁了或者指向了别的对象)std::shared_ptr会销毁它指向的对象。就像垃圾回收机制一样,客户不需要管理被指向的对象的生命周期了,但是和析构函数一样,对象的销毁的时间是确定的。 通过查看引用计数(reference count,一个和资源关联的值,这个值能记录有多少std::shared_ptr指向资源),一个std::shared_ptr能告诉我们它是否是最后一个指向这个资源的指针。std::shared_ptr的构造函数会增加引用计数(通常,而不是总是,请看下面),std::shared_ptr的析构函数会减少引用计数,拷贝operator=既增加也减少(如果sp1和sp2是指向不同对象的std::shared_ptr,赋值操作“sp1 = sp2”会修改sp1来让它指向sp2指向的对象。这个赋值操作最后产生的效果就是:原本被sp1指向的对象的引用计数减少了,同时被sp2指向的对象的引用计数增加了。)如果一个std::shared_ptr看到一个引用计数在一次自减操作后变成0了,这就意味着没有别的std::shared_ptr指向这个资源了,所以std::shared_ptr就销毁它了。 引用计数的存在带来的性能的影响: std::shared_ptr是原始指针的两倍大小,因为它们在内部包含了一个指向资源的原始指针,同时包含一个指向资源引用计数的原始指针。 引用计数的内存必须动态分配。概念上来说,引用计数和被指向的资源相关联,但是被指向的对象不知道这件事。因此它们没有地方来存放引用计数。(这里隐含一个令人愉快的提示:任何对象,即使是built-in类型的对象都能被std::shared_ptr管理)Item 21解释了,当使用std::make_shared来创建std::shared_ptr时,动态分配的花费能被避免,但是这里有一些无法使用std::make_shared的情况。不管哪种方式,引用计数被当成动态分配的数据来存储。 引用计数的增加和减少操作必须是原子的,因为在不同的线程中可能同时有多个reader和writer。举个例子,在某个线程中指向的一个资源的std::shared_ptr正在调用析构函数(因此减少它指向的资源的引用计数),同时,在不同的线程中,一个指向相同资源的std::shared_ptr被拷贝了(因此增加了资源的引用计数)。原子操作通常比非原子操作更慢,所以即使引用计数常常只有一个字节的大小,你应该假设对它们的读写是相当费时的。 不知道我之前写的“std::shared_ptr的构造函数只是“通常”增加它指向的对象的引用计数”有没有刺激到你的好奇心。创建一个指向某个对象的std::shared_ptr总是产生一个额外std::shared_ptr指向这个对象,所以为什么我们不能总是增加它的引用计数呢? move构造函数,这就是原因。
阅读全文