为什么make_shared的大小是两个指针?
如此处代码所示,make_shared返回的对象的大小是两个指针。
但是,为什么 make_shared
不能像下面这样工作(假设 T 是我们要创建共享指针的类型):
make_shared
的结果是one大小的指针,它指向大小为sizeof(int) + sizeof(T) >,其中 int 是引用计数,并且在指针的构造/销毁时递增和递减。
unique_ptr
只是一个指针的大小,所以我不确定为什么共享指针需要两个。据我所知,它需要一个引用计数,通过 make_shared 可以将其与对象本身放在一起。
另外,是否有任何按照我建议的方式实现的实现(无需为特定对象使用 intrusive_ptr
s)?如果不是,我建议的实施被避免的原因是什么?
As illustrated in the code here, the size of the object returned from make_shared is two pointers.
However, why doesn't make_shared
work like the following (assume T is the type we're making a shared pointer to):
The result of
make_shared
is one pointer in size, which points to of allocated memory of sizesizeof(int) + sizeof(T)
, where the int is a reference count, and this gets incremented and decremented on construction/destruction of the pointers.
unique_ptr
s are only the size of one pointer, so I'm not sure why shared pointer needs two. As far as I can tell, all it needs a reference count, which with make_shared
, can be placed with the object itself.
Also, is there any implementation that is implemented the way I suggest (without having to muck around with intrusive_ptr
s for particular objects)? If not, what is the reason why the implementation I suggest is avoided?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
发布评论
评论(4)
引用计数不能存储在shared_ptr
中。 shared_ptr
必须在各个实例之间共享引用计数,因此 shared_ptr
必须有一个指向引用计数的指针。此外,shared_ptr
(make_shared
的结果)没有将引用计数存储在分配对象的同一分配中
。 make_shared
的要点是防止为 shared_ptr
分配两块内存。通常,如果您只执行 shared_ptr
,除了分配的 T
之外,还必须为引用计数分配内存。 make_shared
将所有这些都放在一个分配块中,使用placement new 和delete 创建T
。所以你只得到一次内存分配和一次删除。
但是,shared_ptr
仍然必须能够将引用计数存储在不同的内存块中,因为不需要使用 make_shared
。因此它需要两个指针。
说实话,这不应该打扰你。即使在 64 位环境中,两个指针也没有那么大的空间。您仍然获得了 intrusive_ptr
功能的重要部分(即,不分配内存两次)。
您的问题似乎是“为什么 make_shared
返回 shared_ptr
而不是其他类型?”原因有很多。
shared_ptr
旨在成为一种默认的、包罗万象的智能指针。如果您正在做一些特殊的事情,您可以使用 unique_ptr 或scoped_ptr。或者仅用于函数范围内的临时内存分配。但是 shared_ptr
旨在成为您用于任何严肃的引用计数工作的那种东西。
因此,shared_ptr
将成为接口的一部分。您将拥有采用 shared_ptr
的函数。您将拥有返回 shared_ptr
的函数。等等。
输入make_shared
。根据你的想法,这个函数将返回某种新类型的对象,一个 make_shared_ptr 或其他什么。它有自己的等价于 weak_ptr
的东西,即 make_weak_ptr
。但是,尽管这两组类型共享完全相同的接口,但您不能一起使用它们。
采用 make_shared_ptr
的函数无法采用 shared_ptr
。您可以将 make_shared_ptr
转换为 shared_ptr
,但不能反过来。您无法获取任何 shared_ptr
并将其转换为 make_shared_ptr
,因为 shared_ptr
需要两个指针。如果没有两个指针,它就无法完成其工作。
所以现在你有两组半不兼容的指针。您有单向转换;如果您有一个返回 shared_ptr
的函数,用户最好使用 shared_ptr
而不是 make_shared_ptr
。
为了指针的空间而这样做是不值得的。创建这种不兼容性,只为 4 个字节创建两组指针?这根本不值得造成麻烦。
现在,也许您会问,“如果您有 make_shared_ptr
,为什么还需要 shared_ptr
呢?”
因为 make_shared_ptr
不够。 make_shared
并不是创建 shared_ptr
的唯一方法。也许我正在使用一些 C 代码。也许我正在使用 SQLite3。 sqlite3_open
返回一个sqlite3*
,它是一个数据库连接。
现在,使用正确的析构函数,我可以将 sqlite3*
存储在 shared_ptr
中。该对象将被引用计数。我可以在必要时使用 weak_ptr
。我可以使用从 make_shared
或任何其他接口获得的常规 C++ shared_ptr
来玩我通常会玩的所有技巧。它会完美地工作。
但如果 make_shared_ptr
存在,那么这不起作用。因为我无法从中创建其中一个。 sqlite3*
已经分配;我无法通过 make_shared
来加载它,因为 make_shared
构造一个对象。它不适用于现有的。
哦,当然,我可以做一些 hack,我将 sqlite3*
捆绑在一个 C++ 类型中,该类型的析构函数将销毁它,然后使用 make_shared
创建该类型。但随后使用它就会变得更加复杂:您必须经历另一个间接级别。而且你必须经历制作类型等麻烦;上面的析构函数至少可以使用一个简单的lambda函数。
智能指针类型的激增是需要避免的。您需要一个不可移动的、一个可移动的和一个可复制的共享的。还有一个可以打破后者的循环引用。如果您开始拥有多个此类类型,那么您要么有非常特殊的需求,要么您做错了什么。
我有一个 honey::shared_ptr< /code>
实现在侵入时自动优化为 1 指针的大小。它在概念上很简单 - 从
SharedObj
继承的类型具有嵌入式控制块,因此在这种情况下 shared_ptr
是侵入性的,可以进行优化。它将 boost::intrusive_ptr 与非侵入式指针(例如 std::shared_ptr 和 std::weak_ptr)统一起来。
这种优化之所以可能,是因为我不支持别名(请参阅霍华德的回答)。如果 T
已知在编译时是侵入性的,则 make_shared
的结果可以具有 1 个指针大小。但是,如果已知 T
在编译时是非侵入性的怎么办?在这种情况下,拥有 1 个指针大小是不切实际的,因为 shared_ptr
必须表现得一般才能支持与其对象并排或单独分配的控制块。如果只有 1 个指针,一般行为是指向控制块,因此要获取 T*
您必须首先取消引用控制块,这是不切实际的。
其他人已经说过shared_ptr需要两个指针,因为它必须指向引用计数内存块和指向类型内存块。
我想您要问的是:
当使用 make_shared
时,两个内存块都会合并为一个,并且因为块大小和对齐方式是已知的,并且在编译时固定,一个指针可以从另一个指针计算出来(因为它们有固定的偏移量)。那么为什么标准或 boost 不创建第二种类型,例如 small_shared_ptr
,它只包含一个指针。
是这样吗?
答案是,如果你仔细思考,它很快就会变成一个大麻烦,却收效甚微。如何使指针兼容?一个方向,即将 small_shared_ptr
分配给 shared_ptr
会很容易,反之则极其困难。即使您有效地解决了这个问题,您所获得的微小效率也可能会因往返转换而损失,而这种转换将不可避免地出现在任何严肃的程序中。而且额外的指针类型也使得使用它的代码更难理解。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
在我所知的所有实现中,
shared_ptr
将拥有的指针和引用计数存储在同一内存块中。这与其他答案所说的相反。此外,指针的副本将存储在shared_ptr
对象中。 N1431 描述了典型的内存布局。确实,我们可以仅用一个指针的 sizeof 来构建一个引用计数指针。但是 std::shared_ptr 包含绝对需要两个指针大小的功能。这些功能之一就是这个构造函数:
shared_ptr
中的一个指针将指向r
拥有的控制块。该控制块将包含拥有的指针,该指针不必是p
,并且通常不是p
。shared_ptr
中的另一个指针,即get()
返回的指针,将是p
。这称为别名支持,并在 N2351。您可能会注意到,在引入此功能之前,
shared_ptr
的大小为两个指针。在引入此功能之前,人们可能已经用 sizeof 一个指针实现了shared_ptr
,但没有人这样做,因为它不切实际。 N2351之后,它变成了不可能的。在 之前它不切实际的原因之一N2351 是因为支持:
Here,
p.get()
returns aB*
,并且通常忘记了所有关于输入A
。唯一的要求是A*
可以转换为B*
。B
可以使用多重继承从A
派生。这意味着当从A
转换为B
时,指针本身的值可能会发生变化,反之亦然。在此示例中,shared_ptr
需要记住两件事:get()
时如何返回B*
。A*
。实现此目的的一个非常好的实现技术是将
B*
存储在shared_ptr
对象中,并将A*
存储在控制块中引用计数。In all implementations I'm aware of,
shared_ptr
stores the owned pointer and the reference count in the same memory block. This is contrary to what other answers are saying. Additionally a copy of the pointer will be stored in theshared_ptr
object. N1431 describes the typical memory layout.It is true that one can build a reference counted pointer with sizeof only one pointer. But
std::shared_ptr
contains features that absolutely demand a sizeof two pointers. One of those features is this constructor:One pointer in the
shared_ptr
is going to point to the control block owned byr
. This control block is going to contain the owned pointer, which does not have to bep
, and typically isn'tp
. The other pointer in theshared_ptr
, the one returned byget()
, is going to bep
.This is referred to as aliasing support and was introduced in N2351. You may note that
shared_ptr
had a sizeof two pointers prior to the introduction of this feature. Prior to the introduction of this feature, one could possibly have implementedshared_ptr
with a sizeof one pointer, but no one did because it was impractical. After N2351, it became impossible.One of the reasons it was impractical prior to N2351 was because of support for:
Here,
p.get()
returns aB*
, and has generally forgotten all about the typeA
. The only requirement is thatA*
be convertible toB*
.B
may derive fromA
using multiple inheritance. And this implies that the value of the pointer itself may change when converting fromA
toB
and vice-versa. In this example,shared_ptr<B>
needs to remember two things:B*
whenget()
is called.A*
when it is time to do so.A very nice implementation technique to accomplish this is to store the
B*
in theshared_ptr
object, and theA*
within the control block with the reference count.