std :: shared_ptr作为最后的手段?


59

我只是在观看“ Going Native 2012”流,并且注意到有关的讨论std::shared_ptr。听到Bjarne std::shared_ptr对此持否定态度,以及他的评论是当物体的寿命不确定时应将其用作“最后手段”时,我感到有些惊讶(我认为,他认为这种情况很少见)。

有人愿意进一步解释这一点吗?我们如何std::shared_ptr在没有安全的情况下进行编程并仍然可以安全地管理对象的生命周期?


8
不使用指针?拥有对象的唯一所有者,可以管理生命周期?
Bo Persson

2
显式共享的数据呢?很难不使用指针。在这种情况下,std :: shared_pointer也会做肮脏的“管理生命”
Kamil Klimek'2

6
您是否考虑过少听所提供的建议,而更多地听取该建议背后的论点?他很好地解释了这种建议将在哪种系统中起作用。
Nicol Bolas'2

@NicolBolas:我听了建议和论点,但显然我不明白我的意思。
ronag 2012年

他什么时候说“不得已”?他在(channel9.msdn.com/Events/GoingNative/GoingNative-2012/…)中观看了36分钟的演讲,他说他对使用指针持谨慎态度,但他指的是指针,一般来说,不仅是shared_ptr和unique_ptr,甚至是“常规”指针。他暗示应该首选对象本身(而不是指向分配有new的对象的指针)。您稍后在演讲中想的是什么?
法拉普2014年

Answers:


55

如果您可以避免共享所有权,那么您的应用程序将更加简单易懂,因此不易受到维护期间引入的错误的影响。复杂或不清楚的所有权模型往往会导致难以通过可能难以跟踪的共享状态来跟踪应用程序不同部分之间的耦合。

鉴于此,最好使用具有自动存储持续时间的对象并具有“值”子对象。如果不这样做,unique_ptr可能会是一个很好的选择,shared_ptr即使不是万不得已,它也会在所需工具列表的下方。


5
+1表示问题不在于技术本身(共享所有权),而是它给我们带来的困难,仅是人类,他们随后必须对正在发生的事情进行解读。
Matthieu M.

但是,采用这种方法将严重限制程序员在大多数非平凡的OOP类上应用并发编程模式的能力(由于不可复制性。)“ Going Native 2013”​​中提出了此问题。
rwong

48

Bjarne所生活的世界是一个……学术界,需要一个更好的期限。如果您可以对代码进行设计和结构化,以使对象具有非常深思熟虑的关系层次结构,以使所有权关系僵化且不屈服,则代码会朝一个方向(从高级到低级)流动,而对象仅与低级对象对话。层次结构,那么您将不需要太多shared_ptr。在某些人不得不违反规则的罕见情况下,您会用到它。但是否则,您可以将所有内容粘贴在vectors或使用值语义的其他数据结构中,并将unique_ptrs用于必须单独分配的内容。

虽然这是一个生活的美好世界,但并非所有时间都能做到。如果您不能以这种方式组织代码,因为您要尝试进行的系统设计意味着不可能(或者非常不愉快),那么您将发现自己越来越需要对象的共享所有权。

在这样的系统中,握着裸露的指针并不是……完全没有危险,但是确实会引发问题。最棒的shared_ptr是,它为对象的生存期提供了合理的语法保证。可以打破吗?当然。但是人们也可以做到const_cast。基本护理和喂养shared_ptr应为必须共享所有权的分配对象提供合理的生活质量。

然后,有weak_ptrs,如果没有,则不能使用shared_ptr。如果您的系统是严格结构化的,则可以安全地存储指向某个对象的裸指针,前提是应用程序的结构可确保所指向的对象比您更长寿。您可以调用一个返回指向某个内部或外部值的指针的函数(例如,找到名为X的对象)。在结构正确的代码中,只有保证对象的寿命超过您的寿命,该功能才可用。因此,将裸指针存储在您的对象中就可以了。

由于是刚性并不总是能够在实际系统中实现,你需要一些方法来合理保证寿命。有时,您不需要完全所有权;有时,您只需要能够知道指针是好还是坏。那是weak_ptr进入的地方。在某些情况下,我可以使用unique_ptror boost::scoped_ptr,但是我必须使用a,shared_ptr因为我特别需要给某人一个“易失”的指针。一个指针的生命周期是不确定的,并且他们可以查询该指针何时被销毁。

当世界状况不确定时,一种安全的生存方式。

可以通过某些函数调用来获取指针,而不是通过via来完成weak_ptr吗?是的,但是更容易打破。返回裸指针的函数无法从句法上暗示用户不要做类似于长期存储该指针的操作。返回a shared_ptr也使某人过于简单地简单地存储它并可能延长对象的寿命。weak_ptr但是,返回a 强烈表明,存储shared_ptr您的lock来信是一个……可疑的主意。它不会阻止您执行此操作,但是C ++中的任何操作都不能阻止您破坏代码。weak_ptr提供了一些抵制自然行为的最小阻力。

现在,这并不是说shared_ptr不能滥用;当然可以。特别是pre- unique_ptr,在很多情况下我只是使用a,boost::shared_ptr因为我需要传递一个RAII指针或将其放在列表中。没有移动语义和unique_ptrboost::shared_ptr是唯一的实际解决方案。

您可以在完全没有必要的地方使用它。如上所述,适当的代码结构可以消除对的某些使用需求shared_ptr。但是,如果您的系统无法按原样进行构建,但仍然可以按需进行,shared_ptr则将具有很大的用途。


4
+1:例如看boost :: asio。我认为这种想法扩展到许多领域,您可能在编译时可能不知道哪个UI窗口小部件或异步调用是最后一个放弃对象的对象,而对于shared_ptr则不需要知道。显然,它并不适用于所有情况,只是工具箱中的另一个(非常有用)工具。
Guy Sirton

3
有点迟到的评论;shared_ptr非常适合将c ++与脚本语言(例如python)集成在一起的系统。使用boost::python,在c ++和python端的引用计数协作很大;来自c ++的任何对象仍可以保留在python中,并且不会死。
eudoxos 2012年

1
仅供参考,我对WebKit和Chromium的使用都不了解shared_ptr。两者都使用自己的的实现intrusive_ptr。我之所以提出这一点,是因为它们都是用C ++编写的大型应用程序的真实示例
gman

1
@gman:我发现您的评论很有误导性,因为Stroustrup的异议shared_ptr同样适用于intrusive_ptr:他反对整个所有权的概念,而不是该概念的任何特定拼写。因此,出于这个问题的目的,这是两个确实使用的大型应用程序的真实示例shared_ptr。(而且,更重要的是,它们证明,shared_ptr即使它没有启用,它也很有用weak_ptr。)
ruakh

1
FWIW,为了反驳Bjarne生活在学术界的说法:在我的全部工业生涯(包括共同设计G20股票交易所和仅设计50万播放器MOG)中,我只看到3个确实需要的案例共享所有权。我和Bjarne的比例是200%。
No-Bugs野兔

37

我不相信我曾经用过std::shared_ptr

大多数情况下,对象与某个集合相关联,在整个生命周期中它都属于该集合。在这种情况下,您可以仅使用whatever_collection<o_type>whatever_collection<std::unique_ptr<o_type>>,该集合是对象或自动变量的成员。当然,如果您不需要动态数量的对象,则可以使用固定大小的自动数组。

通过集合进行迭代或对该对象进行任何其他操作都不需要帮助函数来共享所有权...它使用该对象,然后返回,并且调用方保证该对象在整个调用中均保持活动状态。到目前为止,这是主叫方和被叫方之间最常用的合同。


尼科尔·波拉斯(Nicol Bolas)评论说:“如果某个物体抓住裸露的指针,而该物体死亡……哎呀。” 和“对象需要确保该对象在该对象的生命中生存。只能shared_ptr做到这一点。”

我不赞成这种说法。至少不能解决shared_ptr这个问题。关于什么:

  • 如果某个哈希表保留在对象上,并且该对象的哈希码发生更改,那么...糟糕。
  • 如果某个函数正在迭代一个向量,并且向该向量中插入了一个元素,那么。

与垃圾回收一样,默认使用shared_ptr鼓励程序员不要考虑对象之间或函数与调用者之间的约定。需要考虑正确的前提条件和后置条件,并且对象生存期只是更大的一块蛋糕中的一小部分。

对象不会“死亡”,某些代码会破坏它们。抛开shared_ptr问题而不是弄清电话合同是错误的安全。


17
@ronag:我怀疑您已经开始在原始指针会更好的地方使用它,因为“原始指针不好”。但是原始指针还不错。只将拥有对象的第一个指针变成原始指针是不好的,因为那样一来,您就必须手动管理内存,这在存在异常的情况下是不平凡的。但是使用原始指针作为句柄或迭代器就可以了。
Ben Voigt

4
@BenVoigt:当然,传递裸露指针的困难在于您不知道对象的寿命。如果某个对象抓住裸露的指针,并且该对象死亡...糟糕。那正是这种事情shared_ptrweak_ptr旨在避免发生。Bjarne试图生活在一个世界中,即一切都有美好的,明确的一生,而一切都是以此为基础的。如果您可以建立这个世界,那就太好了。但是,事实并非如此。对象需要确保对象在对象的整个生命中都存在。只能shared_ptr这样做。
Nicol Bolas 2012年

5
@NicolBolas:那是虚假的安全。如果函数的调用者没有提供通常的保证:“在函数调用期间,任何外部方都不会触摸此对象”,那么双方都需要就允许哪种类型的外部修改达成一致。 shared_ptr仅减轻了一项特定的外部修改,甚至没有最常见的修改。如果函数调用合同另有规定,则确保对象的生存期正确不是对象的责任。
Ben Voigt 2012年

6
@NicolBolas:如果一个函数创建一个对象并通过指针返回它,则它应该是一个unique_ptr,表示只有一个指向该对象的指针存在并且它具有所有权。
Ben Voigt 2012年

6
@Nicol:如果要在某个集合中查找指针,则可能应该使用该集合中的任何指针类型,如果该集合保存值,则应使用原始指针。如果它正在创建一个对象,并且调用方需要a shared_ptr,则它仍应返回a unique_ptr。从unique_ptr到的转换shared_ptr很容易,但是从逻辑上讲,反向转换是不可能的。
Ben Voigt

16

我宁愿不是绝对地思考(例如“不得已而为之”),而是相对于问题领域。

C ++可以提供许多不同的方法来管理生命周期。其中一些尝试以堆栈驱动的方式重新生成对象。其他一些尝试摆脱这种限制。其中一些是“文字的”,另一些是近似的。

实际上,您可以:

  1. 使用纯价值语义。适用于比较小的物体哪里是什么是很重要的是“价值观”,而不是“身份”,在这里你可以假设两个Person具有相同的name是同一(最好:两个相同的表示)。生命周期是由机器堆栈授予的,最终对程序无关紧要(因为一个名字,无论Person它携带什么)
  2. 使用堆栈分配的对象,以及相关的引用或指针:允许多态,并授予对象生存期。不需要“智能指针”,因为您可以确保在堆栈中保留的结构所指向的对象所指向的对象所指向的对象都不能“指向”对象(首先创建对象,然后创建引用该对象的结构)。
  3. 使用堆栈管理的堆分配对象:这是std :: vector以及所有容器和wat std::unique_ptr所做的(您可以认为它是大小为1的向量)。同样,您承认对象在它们所引用的数据结构之前(之后)开始存在(并结束它们的存在)。

这种方法的缺点是,在执行更深层次的堆栈调用时,对象的类型和数量不能随创建位置的不同而变化。在对象创建和删除是用户活动的结果的所有情况下,所有这些技术都无法“发挥作用”,因此对象的运行时类型不是编译时已知的,并且可能存在引用对象的过度结构。用户要求从更深的堆栈级函数调用中删除。在这种情况下,您必须:

  • 介绍一些有关管理对象和相关引用结构的学科或...
  • 不知何故进入了“逃避基于纯堆栈的生命周期”的阴暗面:对象必须独立于创建它们的函数而离开。并且必须离开... 直到需要他们为止

C ++ isteslf没有监视该事件(while(are_they_needed))的任何本机机制,因此您必须近似:

  1. 使用共享所有权:对象生命绑定到“引用计数器”:如果可以对“所有权”进行分层组织,则可以工作,否则可能存在所有权循环。这就是std :: shared_ptr所做的。和weak_ptr可以用来打破循环。这在大多数情况下都是有效的,但在大型设计中会失败,因为许多设计师在不同的团队中工作,并且没有明确的理由(某种程度上的要求)是谁必须发霉(什么是双重喜欢的链):上一个是由于下一个引用了下一个,还是下一个拥有了上一个引用了下一个?在没有要求的情况下,这些解决方案是等效的,并且在大型项目中,您可能会混淆它们)
  2. 使用垃圾收集堆:您根本不在乎生命周期。您不时地运行收集器,什么是unreachabe被视为“不再需要”,并且……嗯……毁坏了?完成吗?冻结?有很多GC收集器,但我从未找到真正了解C ++的收集器。它们中的大多数释放内存,而不关心对象破坏。
  3. 使用具有适当标准方法接口的C ++垃圾回收器。祝你好运找到它。

转到最后一个解决方案的第一个解决方案,随着组织和维护对象所花费的时间增加,管理对象生存期所需的辅助数据结构的数量也会增加。

垃圾收集器具有成本,shared_ptr较少,unique_ptr较少,并且堆栈管理的对象很少。

shared_ptr“最后的手段”吗?不,不是:不得已是垃圾收集器。 shared_ptr实际上是std::建议的最后选择。但是,如果您遇到我所解释的情况,则可能是最好的解决方案。


9

赫伯·萨特(Herb Sutter)在以后的会议中提到的一件事是,每当您复制一个副本时,都会shared_ptr<>发生连锁的增量/减量。在多核系统上的多线程代码上,内存同步不是无关紧要的。给出选择后,最好使用堆栈值或a unique_ptr<>并传递引用或原始指针。


1
shared_ptr
按左

8
关键是,不要shared_ptr仅仅因为它是标准解决方案,而使用像解决所有内存泄漏问题的灵丹妙药那样。这是一个诱人的陷阱,但是了解资源所有权仍然很重要,除非共享所有权,否则a shared_ptr<>不是最佳选择。

对我来说,这是最不重要的细节。请参见过早优化。在大多数情况下,这不应驱动决策。
Guy Sirton

1
@gbjbaanb:是的,它们处于cpu级别,但是在多核系统上,您正在使缓存无效并强制使用内存屏障。

4
在我从事的一个游戏项目中,我们发现性能差异非常明显,以至于我们需要两种不同类型的引用计数指针,一种是线程安全的,另一种不是。
Kylotan

7

我不记得最后一个“度假胜地”是否是他使用的确切词,但我相信他所说的实际含义是最后一个“选择”:给定明确的所有权条件;unique_ptr,weak_ptr,shared_ptr甚至裸露的指针都有它们的位置。

他们都同意的一件事是,我们(开发人员,书籍作者等)都处于C ++ 11的“学习阶段”,并且正在定义模式和样式。

例如,Herb解释说,我们应该期待一些开创性的C ++书籍的新版本,例如有效的C ++(Meyers)和C ++编码标准(Sutter和Alexandrescu),要花几年的时间,而业界在C方面的经验和最佳实践++ 11推出。


5

我认为他的意思是,每个人只要写了标准指针(例如一种全局替换),就经常写shared_ptr,这已经成为一种普遍现象,并且它被用作替代品,而不是实际设计或至少规划对象的创建和删除。

人们忘记的另一件事(除了上述材料中提到的锁定/更新/解锁瓶颈),仅shared_ptr不能解决循环问题。您仍然可以使用shared_ptr泄漏资源:

对象A包含指向另一个对象A的共享指针。对象B创建A a1和A a2,并分配a1.otherA = a2; 和a2.otherA = a1; 现在,用于创建a1,a2的对象B的共享指针超出范围(例如,在函数末尾)。现在您有一个泄漏-没有其他人引用a1和a2,但是它们互相引用,因此它们的引用计数始终为1,因此您已经泄漏了。

那是简单的示例,当它在真实代码中发生时,通常会以复杂的方式发生。使用weak_ptr有一个解决方案,但是现在有很多人只在任何地方都执行shared_ptr,甚至都不知道泄漏问题,甚至都不知道weak_ptr。

总结一下:我认为OP引用的注释可以归结为:

无论您使用哪种语言(托管,非托管,还是介于中间的诸如引用共享之类的引用计数),都需要了解并有意识地决定对象的创建,生存期和销毁。

编辑:即使这意味着“未知,我需要使用shared_ptr”,您仍然会想到它,并且是有意这样做的。


3

我将从我在Objective-C的经验中回答,Objective-C是一种语言,其中所有对象都被引用计数并分配到堆上。因为有一种处理对象的方法,所以对于程序员而言,事情要容易得多。这样就可以定义标准规则,这些规则遵循这些规则,可以保证代码的鲁棒性并且不会造成内存泄漏。像最近的ARC(自动引用计数)一样,巧妙的编译器优化也可能出现。

我的观点是,shared_ptr应该是您的第一选择,而不是最后的选择。仅当您确定自己在做什么时,才使用默认情况下的引用计数和其他选项。您将提高工作效率,并且代码将更强大。


1

我将尝试回答这个问题:

我们如何在没有std :: shared_ptr的情况下进行编程,并且仍然以安全的方式管理对象的生存期?

C ++具有大量不同的内存处理方式,例如:

  1. struct A { MyStruct s1,s2; };在类范围内使用而不是shared_ptr。这仅适用于高级程序员,因为它要求您了解依赖项的工作原理,并且需要能够充分控制依赖项以将其限制为树的能力。头文件中类的顺序是此方面的重要方面。似乎这种用法对于本机内置的c ++类型已经很普遍了,但由于这些依赖性和类顺序问题,似乎很少使用程序员定义的类。此解决方案也有sizeof问题。程序员将其中的问题视为使用前向声明或不必要的#include的要求,因此许多程序员将退回到劣等的指针解决方案,后来又转向shared_ptr。
  2. 使用MyClass &find_obj(int i);+ clone()代替shared_ptr<MyClass> create_obj(int i);。许多程序员希望创建工厂来创建新对象。shared_ptr非常适合这种用法。问题在于它已经假定使用堆/空闲存储分配的复杂内存管理解决方案,而不是更简单的基于堆栈或对象的解决方案。良好的C ++类层次结构支持所有内存管理方案,而不仅仅是其中一种。如果返回的对象存储在包含对象内,而不使用局部函数作用域变量,则基于引用的解决方案可以工作。应避免将所有权从工厂传递给用户代码。使用find_obj()之后复制对象是处理它的好方法-带有复制参数的普通复制构造函数和普通构造函数(不同类)或多态对象的clone()都可以处理它。
  3. 使用引用代替指针或shared_ptrs。每个c ++类都有构造函数,并且每个引用数据成员都需要初始化。这种用法可以避免多次使用指针和shared_ptrs。您只需要选择内存是在对象内部还是外部,然后根据决策选择结构解决方案或引用解决方案。此解决方案的问题通常与避免使用构造函数参数(这是常见但有问题的做法)有关,并且误解了应如何设计类的接口。

“应该避免将所有权从工厂传递给用户代码。” 当不可能的时候会发生什么呢?“使用引用而不是指针或shared_ptrs。” 不。指针可以重新放置。参考不能。这对类中存储的内容施加了构建时限制。对于很多事情来说,这是不切实际的。您的解决方案似乎非常僵化,对更流畅的界面和使用模式的需求不灵活。
Nicol Bolas 2012年

@Nicol Bolas:遵循上述规则后,引用将用于对象之间的依赖关系,而不是像您建议的那样用于数据存储。依赖关系比数据更稳定,因此我们永远不会陷入您正在考虑的问题。
tp1 2012年

这是一个非常简单的示例。您有一个游戏实体,它是一个对象。它需要引用另一个对象,这是它需要与之交谈的目标实体。但是,目标可能会改变。目标可能在不同地点死亡。实体需要能够处理这些情况。严格的无指针方法甚至无法处理更改目标之类的简单任务,更不用说目标快死了。
Nicol Bolas'2

@nicol bolas:哦,这是不同的处理方式;类的接口支持多个“实体”。您将使用entityarray,而不是对象与实体之间的1:1映射。然后,只需将实体从数组中删除,实体就很容易死亡。在整个游戏中只有很少的实体数组,并且数组之间的依存关系不会经常改变:)
tp1 2012年

2
不,unique_ptr最适合工厂。您可以将unique_ptr变成shared_ptr,但是从逻辑上讲不可能朝另一个方向发展。
Ben Voigt 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.