Item 20: 使用std::weak_ptr替换会招指针悬挂的类std::shared_ptr指针

正文翻译自modern effective
C++,由于水平有限,故无法确保翻译完全正确,欢迎指出错误。谢谢!

博客已经搬至这里啦

拧的是,我们分外容易就可以创造有一个以及std::shared_ptr类似的智能指针,不过,它们不加入被针对资源的共享所有权管理。换句话说,这是一个表现像std::shared_ptr,但却非影响对象引用计数的指针。这样的智能指针需要跟一个针对性std::shared_ptr来说不有的题材做斗争:它对的物或已经深受销毁了。一个确实的智能指针需要通过追踪资源的昂立(也就是说,被指向的靶子不有时时)来缓解是题材。std::weak_ptr正好就是那种智能指针。

若或会合奇怪std::weak_ptr有啊用。当您检查std::weak_ptr的API时,你或会面还出人意料。它看起一点呢未智能。std::weak_ptr不克散引用,不克检查指针是否也空。那是坐std::weak_ptr不是独自的智能指针。它是std::shared_ptr的附加物。

其的互换由降生自便存了。std::weak_ptr通常创建自std::shared_ptr。std::shared_ptr起始化它们平日,它们对和std::shard_ptr指向的一律之地点,可是其不影响其所指向对象的援计数:

auto spw = std::make_shared<Widget>();          //spw被构造之后,被指向的Widget
                                                //的引用计数是1(关于std::make_shared
                                                //的信息,看Item 21)

...

std::weak_ptr<Widget> wpw(spw);                 //wpw和spw指向相同的Widget,引用
                                                //计数还是1

...

spw = nullptr;                                  //引用计数变成0,并且Widget被销毁
                                                //wpw现在是悬挂的

悬挂的std::weak_ptr被称作失效的(expired)。你会直接检查她:

if(wpw.expired())...                            //如果wpw不指向一个对象

而以看std::weak_ptr指向的目的,你时要检讨看之std::weak_ptr是否曾经失效了依旧还不曾失效(也就是,它并未悬挂)。想法总是比做起来大概,因为std::weak_ptr没有解除引用操作,所以无办法写有相应的代码。固然会写出来,把消除引用和检讨分离开来会见导致竞争规则:在调用expired和解引用操作中,其它一个线程可能再赋值或者销毁std::shared_ptr往日对的目的,因而,会招致你想解引用的靶子吃灭绝。这样的话,你的消引用操作将生出不定义行为。

你要的凡一个原子操作,它能检查看std::weak_ptr是否失效了,并吃你可以看它对的靶子。从一个std::weak_ptr来创造std::shared_ptr就可以达标这样的目标。你有的std::shared_ptr是什么样的,倚重让以你用std::weak_ptr来创建std::shared_ptr时是不是就失效了。操作有三三两三种植模式,一种是std::weak_ptr::lock,它回到一个std::shared_C++,ptr。如果std::weak_ptr已经失效了,std::shared_ptr会是null:

std::shared_ptr<Widget> spw1 = wpw.lock();      //如果wpw已经失效了,spw1是null

auto spw2 = wpw.lock();                         //和上面一样,不过用的是auto

别一样种样式是参数为std::weak_ptr的std::shared_ptr的构造函数。这样情状下,假诺std::weak_ptr已经失效了,会出一个杀抛来:

std::shared_ptr<Widget> spw3(wpw);          //如果wpw已经失效了,抛出一个
                                            //std::bad_weak_ptr异常

而是若也许仍旧针对std::weak_ptr的用处感到奇怪。考虑一个厂子函数,这一个函数按照唯一的ID,发生一个针对性只读对象的智能指针。与Item
18的提出相契合,考虑工厂函数的回来路,它回到一个std::unique_ptr:

std::unique_ptr<const Widget> loadWidget(WidgetId id);

万一loadWidget是一个昂贵的调用(比如,它实施文书操作仍然I/O操作)并且针对ID的往往使用是许的,我们好做一个理所当然的优化:写一个函数,这么些函数做loadWidget做的事,可是其呢缓存下它们回到的结果。但是把具有请求的Widget都缓存下来会促功效用问题,所以任何一个理所当然之优化是:当Widget不再以时,销毁它的苏醒存。

于这缓存工厂函数,一个std::unique_ptr的归路是不够恰当的。调用者应该收一个针对缓存对象的智能指针,可是缓存也欲一个指针来针对对象。缓存的指针需要以外挂的当儿会察觉到,因为当工厂的客户将工厂回到的指针用完后,对象将会合受销毁,然后在缓存中相应的指针将会晤挂。由此缓存指针应该是一个std::weak_ptr(当指针悬挂的下可以有察觉)。这意味工厂的回来值类型应该是一个std::shared_ptr,因为std::weak_ptr只有当目的的生命周期被std::shared_ptr管理之时节,才会检查自己是不是悬挂。

此间为出一个缓存版本的loadWidget的快捷实现:

std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
    static std::unordered_map<WidgetID, 
                              std::weak_ptr<const Widget> cache;

    auto objPtr = cache[id].lock();     //objPtr是一个std::shared_ptr 
                                        /它指向缓存的对象(或者,当
                                        //对象不在缓存中时为null)

    if(!objPtr){                        //吐过不在缓存中
        objPtr = loadWidget(id);        //加载它
        cache[id] = objPtr;             //缓存它
    }
    return objPtr;
}

这么些实现选用了C++11之一律栽哈希容器(std::unordered_map),即使其并未显得WidgetID的哈希函数以及对比函数,但他们依旧会见叫实现出来的。

fastLoadWidget的落实忽略了一个实际,那即使是缓存可能积累有失效了的std::weak_ptr(对应之Widget已经不复受利用(因此这么些Widget已经灭绝了))。实现能叫越来越优化,可是相比花费时间以那个题目(对std::weak_ptr的明没有额外的升级换代)上,让咱考虑次只利用意况:观望者设计格局。那些设计情势最关键的零部件就是目标(subject,目的的状态恐怕会晤有变更)和观望者(observer,当目的的状态爆发变动时,阅览者会受打招呼)。大多数落实着,每个目标包含一个数量成员,那些成员具有指向观望者的指针。这使目标在状态有变动之上,通告起更易于。目的对控制他们的观察者的生命周期没有趣味(也就是,当他俩销毁时),不过它们对它们的观看者是否已经灭绝了相当有趣味,这样它们就是无会合尝试去拜访观望者了。一个合理之宏图是:让每个指标有一个容器,这些容器中弄虚作假了依赖为其观看者的std::weak_ptr,因此,这让对象以使用一个指南针前能确定它是不是悬挂的。

说到底一个std::weak_ptr的动例子是:考虑一个有关A,B,C的数据结构,A和C共享B的所有权,由此都持有std::shared_ptr指向B:

C++ 1

若果从B指向A的指针同样暴发因而,这多少个指针应该是啊类型的为?

C++ 2

这里暴发二种植拔取:

  • 一个原始指针。用这种措施,假如A销毁了,不过C依旧指向B,B将有指向A的悬挂指针。B不相会发现,所以B可能随便发现地破引用这悬挂指针。这将出不定义的行事。

  • 一个std::shared_ptr。在这种规划下,A和B互周旋有指向对方的std::shared_ptr。这来了std::shared_ptr的巡回引用(A指向B,B指向A),这会阻拦A和B被销毁。即便A和B不能从另外数据结构得到(比如,C不再指向B),A和B的援计数都依旧1.比方当时有了,A和B将受泄露,实际上:程序用不再会顾它们,这么些资源为以不能叫回收。

  • 一个std::weak_ptr。这防止了上边的一定量独问题。假若A被销毁了,B中,指向A的指针将悬挂,可是B能觉察到。此外,虽然A和B会相互指向对方,B的指针也未晤面影响A的援计数,因而A不再受针对时,B也非会师阻止A被销毁。

使用std::weak_ptr是三单选项中极好之一个。不过,使用std::weak_ptr来预防std::shared_ptr的匪常见的巡回引用是勿值得的。在严格分层的数据结构中,比如树,子节点通常只是让其的父节点拥有。当一个父节点被销毁时,它的子节点也应吃灭绝。因而自父节点至子节点的连日平时被代表为std::unique_ptr。从子节点到父节点的链接能叫张掖地促成为原始指针,因为一个子节点的生命周期不应有比她的父节点长。因而此没有子节点对悬挂的父指针举行割除引用的高风险。

自然,不是拥有因指针的数据结构都是严谨分层的,当这种情景有常,就比如面的缓存和寓目者链表的贯彻平等,大家领略std::weak_ptr已经摸索了。

从今效用的看法来拘禁,std::weak_ptr和std::shared_ptr以真相上是一致的。std::weak_ptr对象和std::shared_ptr一样大,它们和std::shared_ptr使用同样的决定块(看Item
19),并且协会,析构,赋值等操作为事关到引用计数的原子操作。这说不定会师给你觉得意外,因为自以那Item的同发端就是写了std::weak_ptr不与引用计数的乘除。我勾勒的实际不是杀意思,我形容的凡,std::weak_ptr不参加共享对象的所有权,因而不汇合潜移默化于指向对象的援计数。控制块被实际还有第二只援计数,这第二只援计数是std::weak_ptr所保障的。细节部分,请继续羁押Item
21。

            你要牢记的从事
  • 使用std::weak_ptr替换那一个会造成悬挂的类std::shared_ptr指针。
  • 使用std::weak_ptr的私房意况包括缓存,观看者链表,以及预防std::shared_ptr的巡回引用。