【问题标题】:Was the raw-pointer constructor of shared_ptr a mistake?shared_ptr 的原始指针构造函数是错误的吗?
【发布时间】:2016-05-22 17:45:24
【问题描述】:

事后看来,给定make_shared,如果 C++11 引入了 shared_ptr,它是否会有一个采用原始指针的构造函数?

是否有支持这个构造函数的有力论据或用例?

它可以避免 exception-safetymemory allocation/performance advantage 使用 make_shared 的有据可查的陷阱。

我相信通过make_shared 要求shared_ptr 构造的另一个优点是它可以是引擎盖下的单个指针,降低其内存使用并使atomic_compare_exchange 之类的东西更简单(并且可能更有效) . (见presentation from C++Now

我了解基本上是 intrusive_ptr(对象和控制块合并)的 shared_ptr 将缺少当前 std::shared_ptr 具有的功能。喜欢:

  1. 将对象与控制块分开释放的能力(如果您长期使用weak_ptrs,这很好)

  2. 与提供原始指针的库的兼容性以及释放它们的责任

  3. 使用自定义删除器(或无删除器,对于非拥有指针)保存任意资源的能力

  4. 在保持父对象活动的同时指向子对象(例如成员)的能力。

我的建议是,这些功能可能不够常用(或者在将其用作RAII-wrapper 的情况下)可能不是最合适的,以保证额外的成本:

  1. 指向控制块的单独指针
  2. (可能)更复杂的 atomic_compare_exchange 逻辑,可能不值得。

C++98 世界中(引入了 shared_ptr),make_shared 不太实用且对用户不太友好(缺少完美的转发需要引用包装器,并且缺少可变参数模板使得实现笨拙)。

【问题讨论】:

  • 如果您无法控制对象构造怎么办? (假设您正在管理 C 库中的资源。)
  • make_shared 是在 C++11 中引入的。
  • @NicolBolas 对,shared_ptr 是在 C++03 中引入的
  • @Arvid 不,不是

标签: c++ c++11 shared-ptr make-shared


【解决方案1】:

事后看来,给定make_shared,如果 C++11 引入了 shared_ptr,它是否会有一个采用原始指针的构造函数?

如果你不控制对象的分配怎么办?如果您需要使用自定义删除器怎么办?如果您需要列表初始化而不是括号怎么办?

make_shared 不处理这些情况。

此外,如果您使用weak_ptr,则通过make_shared 分配的shared_ptr 将不会释放任何内存,直到所有weak_ptrs 也被销毁。因此,即使您有一个普通的共享指针,上面都不适用,您可能仍然更喜欢原始指针构造函数。

如果您的类型为operator newoperator delete 提供重载,则可能是另一种情况。这些可能使其不适合 make_shared,因为不会调用这些重载 - 并且可能它们的存在是有原因的。

【讨论】:

  • 我建议 OP 阅读 Scott Myer 的 Effective Modern C++ 以获得对设计原理的全面描述。
  • 说了这么多,make_shared 可能是正常情况,原始指针情况是例外。
  • @Martin 当然,但是说你应该更喜欢 make_shared 和建议替代方案是设计缺陷之间存在很大差异。
  • @Arvid 不,我不是这么说的(也不是真的 - 它不是存储的两倍,引用计数逻辑是相同的 - 它们只是在不同的时间以不同的方式释放内存)。在某些情况下,make_shared 不是一个选项 - 这不是与成本相关的论点。
  • @Arvid 好的,当然。如果只有make_shared()-able,shared_ptr 就不会那么有用了。但是make_shared 是一种优化,而不是一个真正的构造函数——因此使用成本会使推理倒退。当然,我们付出的“成本”是让优化不如它可能的那么好 - 作为回报,让类更有用。
【解决方案2】:

您的逻辑问题在于认为shared_ptr 在托管指针和get 指针之间存在区别的原因是make_shared 不可用。因此,如果我们强制每个人使用make_shared 来创建shared_ptr,我们就不需要这种区别了。

这是不正确的。

您可以在没有这种区别的情况下实现shared_ptr 的基于指针的构造函数。毕竟,在初始创建托管shared_ptr 时,get 指针和托管指针是相同的。如果您希望shared_ptr 成为sizeof(T*),您可以让shared_ptr 从托管块中获取get 指针。这与 T 是否嵌入托管块无关。

因此,区别与make_shared 以及将T 嵌入与托管块相同的内存中的能力确实完全无关。或者更确切地说,缺乏它。

不,托管指针和get 指针之间的区别是创建的,因为它向shared_ptr 添加了功能。重要的。您列出了其中一些,但您错过了其他:

  • 拥有shared_ptr 到基类的能力。那就是:

    shared_ptr<base> p = make_shared<derived>(...);
    

    为此,您必须区分特定实例指向的内容和控制块所控制的内容。

  • static_pointer_castdynamic_pointer_cast(以及 C++17 中的 reinterpret_pointer_cast)。这些都依赖于托管指针和get 指针之间的区别。

    • 这还包括基类中的enable_shared_from_this
  • 一个shared_ptr 指向一个类型的成员子对象,该类型本身由shared_ptr 管理。同样,它要求托管指针与get 指针不同。

您似乎也忽略了管理不是您创建的指针的能力。这是一个关键能力,因为它允许您与其他代码库兼容。在内部,您可以使用 shared_ptr 来管理由 1998 年编写的库所做的事情。

按照您的方式,您将代码分为两个时期:C++11 之前和 C++11 之后。您的 shared_ptr 不会对任何未明确为 C++11 编写的代码执行任何操作。

而将所有这些特性封装成一个类型的事情是这样的:

你不需要另一个。

shared_ptr,因为它满足了如此多的需求,几乎可以在任何地方有效地使用。它可能不是绝对最有效的类型,但 几乎在所有情况下都能胜任。而且这样做并不慢。

它通过多态处理共享所有权。它处理成员对象的共享所有权。它处理您未分配的内存的共享所有权。它处理具有特殊分配/释放需求的内存的共享所有权。以此类推。

如果您需要共享所有权语义,并且需要它工作shared_ptr 每次都会为您提供支持。有了你提出的想法,总会有一些限制,妨碍你完成工作。

默认情况下应该优先使用有效的类型而不是无效的类型。

【讨论】:

  • 他们不应该为这些情况使用 unique_ptr 构造函数吗?
  • @Bwmat:你说的是什么情况?
  • 可能是为了将所有权转移给 shared_ptr。尽管如此,虽然这种额外的间接可能不会降低运行时效率,但它实际上也没有多大帮助。
【解决方案3】:

std::shared_ptr 不仅仅在堆上分配对象。

考虑将其用作自动关闭的共享文件句柄:

#include <cstdio>
#include <memory>


int main()
{
  auto closer = [](FILE* fp) { std::fclose(fp); };
  auto fp = std::shared_ptr<FILE>(std::fopen("foo.txt", "r"),
                                  closer);
}

【讨论】:

  • “它是”是“它是”的缩写。这不是所有格形式。
  • @LightnessRacesinOrbit mea culpa!
猜你喜欢
  • 2021-11-05
  • 2020-03-14
  • 2021-07-15
  • 1970-01-01
  • 2019-12-13
  • 2019-11-02
  • 2021-11-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多