【问题标题】:C++ shared_ptr vs. Python objectC++ shared_ptr 与 Python 对象
【发布时间】:2015-09-02 20:51:50
【问题描述】:

据我所知,shared_ptr 的使用通常不鼓励使用,因为不小心使用它们会导致潜在的错误(除非你对显着的好处和仔细检查的设计有很好的解释)。

另一方面,Python 对象似乎本质上是 shared_ptrs(ref_count 和垃圾回收)。

我想知道是什么让它们在 Python 中运行良好,但在 C++ 中却存在潜在危险。换句话说,Python 和 C++ 在处理 shared_ptr 方面有什么区别,在 C++ 中不鼓励使用它们,但在 Python 中不会引起类似问题?

我知道例如Python 会自动检测对象之间的循环,从而防止在 C++ 中悬挂循环 shared_ptrs 可能导致的内存泄漏。

【问题讨论】:

    标签: python c++ programming-languages shared-ptr


    【解决方案1】:

    “我知道,例如 Python 会自动检测循环”——这就是使它们工作良好的原因,至少就“潜在错误”与内存泄漏有关而言。

    除此之外,与 Python 程序相比,C++ 程序更常在严格的性能限制下编写(IMO 是不同的真实需求与一些相当虚假的经验法则差异的组合,但这是另一回事)。我使用的相当大比例的 Python 对象并不严格需要引用计数,它们只有一个所有者,unique_ptr 就可以了(或者就此而言是类类型的数据成员)。在 C++ 中(由编写您正在阅读的建议的人)认为值得利用性能优势和明确简化的设计。在 Python 中,这通常不被视为问题,您支付性能费用并保留稍后决定共享它的灵活性,毕竟不需要任何代码更改(我的意思是,除了获取比原始文件更有效的额外引用)。

    顺便说一句,在任何语言中,共享的可变对象都有与之相关的“潜在错误”,如果您在不查看对象时忘记了哪些对象会更改或不会更改。我不仅仅指竞争条件:即使在单线程程序中,您也需要注意 C++ 谓词不应更改任何内容,并且您(通常)不能在迭代容器时对其进行变异。不过,我不认为这是 C++ 和 Python 之间的区别。相反,在某种程度上,您应该对 Python 中的共享对象也稍有警惕,并且当您激增对对象的引用时,至少要了解您这样做的原因。

    那么,关于您链接到的问题中的问题列表:

    这主要是因为在 C++ 中你必须明确地做一些事情来获得引用计数,如果你不要求它,你就不会得到它。这提供了 Python 没有为程序员提供的几个错误机会,因为它只是为你做的。如果您正确使用shared_ptr,那么除了存在不与之合作的库之外,C++ 中也不会出现这些问题。出于这些原因而谨慎使用它的人基本上是在说他们害怕他们会错误地使用它,或者至少比他们会误用某些替代品更害怕。许多 C++ 编程都在相互交换不同的潜在错误,直到你想出一个你认为自己有能力执行的设计。此外,它具有“不为不需要的东西买单”作为设计理念。在这两个因素之间,如果没有非常好的解释、显着的好处和经过仔细检查的设计,您就不会做任何事情。 shared_ptr 也不例外 ;-)

    【讨论】:

    • 我知道这可能是最常见的错误,但如果我没记错的话,循环引用并不是由 shared_ptr 引起的唯一常见错误。所以例如如果我们假设地添加一个周期性的 shared_ptr 循环检测,是否可以解决 Python 对象不会发生的 shared_ptr 的危险?或者还有更多关于为什么它们在 Python 中运行良好的原因。关于 shared_ptr 的危险性和 Google 的 C++ 风格指南的问题,我的印象是它不是唯一的。
    • @Kaveh:肯定会有所帮助,那里有垃圾收集智能指针库,当然现在标准也允许 C++ 实现进行垃圾收集。但在有人明确他们所讨论的潜在错误类型之前,实际上不可能评估在 Python 中如何解决相同的问题。请注意,Google 的 C++ 风格指南对于使用 C++ 功能出奇地谨慎,并不反映 C++ 的典型用法。
    • 例如stackoverflow.com/q/701456 中提到的问题(是的,我知道样式指南是针对 Google 现有代码库量身定制的。:)
    • 优秀的答案。从本质上讲,我的经验是,随着时间的推移,您几乎会犯上述所有错误并纠正您的设计以避免这种情况。例如:将构造函数设为私有,并强制执行只给出shared_ptr 的朋友make 方法。没有机会获得非引用对象,除非您明确地为通过get() 获得的对象创建共享指针(这将是一个明显的错误),否则您不必将shared_ptr 指向同一个对象。显然,您只会对需要处理的大型对象执行此操作。
    • @Marcus:就个人而言,我会强制进行堆分配的类型很少,即使这样我也会让工厂返回unique_ptr 并让用户“促进”仅当对象实际上是共享的时才到shared_ptr。但这是支持更多用例的细节,我同意根据经验,您可以针对您所知道的错误进行防御性编码。如果该类型有一个公共构造函数并且有一些滥用new 而不是使用make_sharedmake_unique 或其他一些“安全”构造成语,那么可以说这是他们的错,应该是一个审阅者磁铁:-)跨度>
    【解决方案2】:

    据我所知,shared_ptr 的使用通常不鼓励使用,因为不小心使用它们会导致潜在的错误(除非你对显着的好处和仔细检查的设计有很好的解释)。

    我不会同意的。除非您有充分的理由不这样做,否则倾向于普遍使用这些智能指针。

    shared_ptr 在 C++ 中不鼓励使用,但在 Python 中不会引起类似问题?

    好吧,我不知道你最喜欢的大型信号处理框架生态系统,但GNU Radio 将 shared_ptrs 用于他们的所有,这是 GNU Radio 架构的核心元素。实际上,块是类,具有private 构造函数,只能由friend make 函数访问,该函数返回shared_ptr。我们在这方面没有遇到任何问题——GNU Radio 有充分的理由采用这样的模型。现在,我们没有一个地方可以让用户尝试使用已释放的块对象,也没有一个块被泄露。不错!

    此外,我们对一些 C++ 类型使用 SWIG 和网关类,这些类型不能仅用 Python 类型很好地表示。所有这些在 C++ 和 Python 双方都非常有效。事实上,它工作得非常好,我们可以将 Python 类用作 C++ 运行时中的块,包装在 shared_ptr 中。

    此外,我们从未遇到过性能问题。 GNU Radio 是一个高速率、高度优化、高度多线程的框架。

    【讨论】:

    • @MarcusMüller 但也许 gnuradio 确实需要它们。我的错误观点是您必须考虑 C++ 中的所有权,而共享所有权通常不是最佳解决方案。对于习惯使用 python、Java 或其他语言的人来说,这可能很难掌握,因此他们可能对过度使用 shared_ptr 有偏见。
    • @juanchopanza 啊!好的,是的,我同意。 GNU Radio 中所有权的问题在于它是一个非常面向有向图的应用程序,有很多缓冲区,其中有块作为输入和输出。然而,块的创建是从外部发生的,并且通常不清楚用户应用程序是否想要保留块,或者实例是否可以在执行后解构; shared_ptrs 通过引用计数和作用域来解决这个问题。
    • @Kaveh 正如史蒂夫杰索普在他的回答中提到的那样,在 python 中大多数东西都是共享的。因此,您不必在没有选择的意义上考虑它。但是您确实必须从某种意义上考虑它,即您必须意识到您对对象所做的事情可能会影响一百万英里外的代码。
    • @Marcus:出于兴趣,gnuradio 是否有一些通用技巧来防止未收集的参考循环?是weak_ptr,还是设计中有一些有用的属性可以确保您的图形是树木/森林?
    • 嗯,我们的图必须是无循环的,但不是为了引用目的,而是为了处理/因果关系。块对象本身通常不持有彼此的引用,因此很少有机会建立循环。事实上,这些对象仅通过循环缓冲区(或我们映射到假循环缓冲区的内容)或通过使用线程安全队列的消息传递(通常每个块在其自己的线程中“运行”)进行通信。调度程序的工作是代理数据的输入和输出。 @SteveJessop
    猜你喜欢
    • 2011-09-27
    • 1970-01-01
    • 2017-10-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-11
    • 2020-11-04
    • 1970-01-01
    相关资源
    最近更新 更多