C++ shared_ptr与Python对象比较

3
据我所知,由于粗心使用shared_ptr可能导致潜在的bug(除非您有一个真正好的解释来证明它能带来显著的好处并且经过仔细检查的设计),因此通常不鼓励使用shared_ptr。
另一方面,Python对象似乎本质上就是shared_ptrs(具有引用计数和垃圾回收)。
我想知道的是,是什么使它们在Python中运行良好,但在C++中潜在地危险。换句话说,Python和C++在处理shared_ptr时有何不同,使得在C++中不鼓励使用它们,但在Python中不会引起类似的问题?
例如,我知道Python会自动检测对象之间的循环引用,防止内存泄漏,而这种循环引用可能会导致C++中悬空的循环共享指针的问题。
2个回答

9
“我知道比如Python会自动检测循环引用,这正是使它们能够良好工作的原因,至少就“潜在错误”与内存泄漏有关而言是这样的。”
此外,C++程序通常比Python程序更多地受到紧密的性能约束(依我之见,这是由于不同的真实需求和一些相当虚假的规则差异组成的)。“我使用的Python对象中相当高的比例并不严格需要引用计数,它们只有一个所有者,并且使用unique_ptr合适(或者同类数据成员)。在C++中,人们认为(你正在阅读的建议者)这个性能优势和明确简化的设计值得付出。在Python中,这通常不被认为是问题,你为此付出性能代价,并保留后续决定是否共享的灵活性,无需进行任何代码更改(除了获取超过最初引用的其他引用之外,我的意思是)。”
顺便说一下,在任何语言中,共享可变对象都存在“潜在错误”,如果你在看不到它们时追踪不了哪些对象会或不会发生变化。我不仅指竞争条件:即使在单线程程序中,你也需要意识到 C++ Predicate 不应更改任何内容,而且你(通常)不能在迭代一个容器时突变它。我不认为这是C++和Python之间的差异。相反,你应该对在Python中共享对象略微保持警惕,在繁殖对象的引用时至少要理解为什么这样做。
那么,让我们来看一下你链接到的问题列表: 大部分原因与C++需要显式地执行引用计数有关,如果不请求它,则不会得到引用计数。这为程序员提供了几个错误的机会,而Python则不会出现这些问题,因为它会自动处理。如果正确使用shared_ptr,则除了存在不与之配合的库之外,在C++中也不会出现这些问题。那些因此而谨慎使用它的人基本上是在说他们害怕使用不当,或者至少比误用其他替代方案更害怕。C++编程的很大一部分是将不同的潜在错误相互权衡,直到设计出一种您认为自己能够执行的设计。此外,它还具有“不要为您不需要的东西付费”的设计哲学。在这两个因素之间,您不会做任何事情,除非有一个真正好的解释、一个重要的好处和一个经过仔细检查的设计。shared_ptr也不例外;-)

我知道这可能是最常见的错误,但如果我没记错,循环引用并不是由shared_ptr引起的唯一常见错误。所以,例如,如果我们“假设”添加了定期的shared_ptr循环检测,那么是否可以解决shared_ptr的危险,这种危险在Python对象中不存在?还是说Python中的共享指针之所以能够良好运作,原因更多?从有关shared_ptr危险性的问题和Google的C++样式指南中的印象来看,它并不是唯一的错误。 - Kaveh
@Kaveh:这肯定会有所帮助,因为有垃圾回收的智能指针库,当然标准现在也允许C++实现进行垃圾回收。但是,在某人指定他们所说的潜在错误类型之前,就不可能评估Python中如何解决相同问题。请注意,Google的C++风格指南对于使用C++特性非常谨慎,并且不反映C++的典型用法。 - Steve Jessop
例如在https://dev59.com/kXRB5IYBdhLWcg3wJklC/中提到的问题(是的,我知道这个风格指南有点针对谷歌现有的代码库。 :) - Kaveh
优秀的回答。从本质上讲,我的经验是随着时间的推移,您会犯以上几乎所有的错误并纠正您的设计以避免这种情况。例如:将构造函数设置为私有,并强制实施一个友元“make”方法,该方法仅提供“shared_ptr”。没有获得非引用计数对象的机会,除非您明确为通过“get()”获得的对象创建共享指针(这将是一个明显的错误),否则您无法将同一对象的“shared_ptr”用于。显然,您只需要处理大型对象。 - Marcus Müller
@Marcus:个人而言,我只会强制将非常少的类型分配到堆上,即便是这样,我也会让工厂返回一个unique_ptr,并只有在对象实际上是共享的情况下才让用户“升级”为shared_ptr。但这是为了支持更多用例的细节。我同意,通过经验,您可以编写防御性代码来抵御您已知的错误。如果该类型具有公共构造函数,并且某些人滥用new而不是使用make_sharedmake_unique或其他“安全”的构造习惯,则可以认为这是他们的错,应该由审核者验证。 - Steve Jessop
是的,但是在像GNU Radio这样多元化的社区中,您不会得到一个审查过程,@SteveJessop,您只会得到一个整个生态系统的out-of-tree模块,它(希望)遵循核心开发人员设置的编码指南(主要是通过示例)。因此,在非性能关键部分中难以犯错对于扩大社区至关重要,而在性能关键部分坚持使用原始内存缓冲区使软件成为在硬实时环境下传输饱和10千兆以太网线路数据的正确选择。 - Marcus Müller

1
据我所知,由于对它们的粗心使用可能导致潜在的错误(除非您有一个真正好的解释来证明显著的优点和经过仔细检查的设计),因此通常不鼓励使用shared_ptr。
我不太同意。趋势是普遍使用这些智能指针,除非你有非常好的理由不这样做。
在C++中,shared_ptr的使用被认为是不鼓励的,但在Python中没有引起类似的问题?
嗯,我不知道您最喜欢的大型信号处理框架生态系统如何,但GNU Radio使用shared_ptr用于其核心元素——块,这是GNU Radio架构的核心元素。实际上,块是类,具有私有构造函数,仅可由友元make函数访问,该函数返回shared_ptr。我们没有遇到问题——GNU Radio有充分的理由采用这种模型。现在,我们没有一个地方用户尝试使用已释放的块对象,也没有一个块泄漏。很好!
此外,我们使用SWIG和一个网关类来处理一些无法很好地表示为Python类型的C++类型。这在C++和Python两侧都非常有效。事实上,它非常有效,以至于我们可以将Python类作为块用于C++运行时,在shared_ptr中包装。
此外,我们从未遇到过性能问题。GNU Radio是一个高速率、高度优化、大量多线程框架。

2
@MarcusMüller 但也许它们在gnuradio中真的是必需的。我的观点不太明确,即您必须考虑C++中的所有权,而共享所有权通常不是最佳解决方案。对于习惯于Python、Java或其他语言的人来说,这可能很难理解,因此他们可能有过度使用shared_ptr的偏见。 - juanchopanza
1
@juanchopanza 啊!好的,是的,我同意。GNU Radio中所有权的问题在于它是一个非常有向图导向的应用程序,具有许多将块作为输入和输出的缓冲区;然而,块的创建发生在外部,并且通常不清楚用户应用程序是否想保留块,还是实例可以在执行后被销毁; shared_ptrs通过引用计数和范围解决了这个问题。 - Marcus Müller
1
正如Steve Jessop在他的回答中所暗示的那样,在Python中,大多数东西都是共享的。因此,您不必认为您没有选择。但是,您必须意识到,您对对象所做的更改可能会影响到遥远的代码。 - juanchopanza
1
@Marcus:感兴趣的是,gnuradio有没有一些通用技巧来防止未收集的引用循环?它是否到处都是“weak_ptr”,还是设计中有一些有用的属性可以确保您的图是树/森林? - Steve Jessop
1
啊,我们的图必须是无环的,但不是为了引用计数目的,而是为了处理/因果方面。块对象本身通常不会相互持有引用,因此很少有机会构建循环。实际上,这些对象仅通过循环缓冲区(或我们 mmap 以模拟 circ 缓冲区)或通过使用线程安全队列进行消息传递来通信(通常,每个块“运行”在自己的线程中)。调度程序的工作是代理数据的进出。@SteveJessop - Marcus Müller
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接