为什么make_shared的大小是两个指针?

发布于 11-26 17:16 字数 547 浏览 3 评论 0原文

此处代码所示,make_shared返回的对象的大小是两个指针。

但是,为什么 make_shared 不能像下面这样工作(假设 T 是我们要创建共享指针的类型):

make_shared的结果是one大小的指针,它指向大小为sizeof(int) + sizeof(T) >,其中 int 是引用计数,并且在指针的构造/销毁时递增和递减。

unique_ptr 只是一个指针的大小,所以我不确定为什么共享指针需要两个。据我所知,它需要一个引用计数,通过 make_shared 可以将其与对象本身放在一起。

另外,是否有任何按照我建议的方式实现的实现(无需为特定对象使用 intrusive_ptrs)?如果不是,我建议的实施被避免的原因是什么?

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 size sizeof(int) + sizeof(T), where the int is a reference count, and this gets incremented and decremented on construction/destruction of the pointers.

unique_ptrs 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_ptrs for particular objects)? If not, what is the reason why the implementation I suggest is avoided?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

楠木可依2024-12-03 17:16:04

在我所知的所有实现中,shared_ptr 将拥有的指针和引用计数存储在同一内存块中。这与其他答案所说的相反。此外,指针的副本将存储在 shared_ptr 对象中。 N1431 描述了典型的内存布局。

确实,我们可以仅用一个指针的 sizeof 来构建一个引用计数指针。但是 std::shared_ptr 包含绝对需要两个指针大小的功能。这些功能之一就是这个构造函数:

template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept;

    Effects: Constructs a shared_ptr instance that stores p
             and shares ownership with r.

    Postconditions: get() == p && use_count() == r.use_count()

shared_ptr 中的一个指针将指向 r 拥有的控制块。该控制块将包含拥有的指针,该指针不必是p,并且通常不是pshared_ptr 中的另一个指针,即 get() 返回的指针,将是 p

这称为别名支持,并在 N2351。您可能会注意到,在引入此功能之前,shared_ptr 的大小为两个指针。在引入此功能之前,人们可能已经用 sizeof 一个指针实现了 shared_ptr,但没有人这样做,因为它不切实际。 N2351之后,它变成了不可能的。

之前它不切实际的原因之一N2351 是因为支持:

shared_ptr<B> p(new A);

Here, p.get() returns a B*,并且通常忘记了所有关于输入A。唯一的要求是 A* 可以转换为 B*B 可以使用多重继承从 A 派生。这意味着当从 A 转换为 B 时,指针本身的值可能会发生变化,反之亦然。在此示例中,shared_ptr 需要记住两件事:

  1. 调用 get() 时如何返回 B*
  2. 如何在需要时删除 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 the shared_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:

template<class Y> shared_ptr(const shared_ptr<Y>& r, T *p) noexcept;

    Effects: Constructs a shared_ptr instance that stores p
             and shares ownership with r.

    Postconditions: get() == p && use_count() == r.use_count()

One pointer in the shared_ptr is going to point to the control block owned by r. This control block is going to contain the owned pointer, which does not have to be p, and typically isn't p. The other pointer in the shared_ptr, the one returned by get(), is going to be p.

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 implemented shared_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:

shared_ptr<B> p(new A);

Here, p.get() returns a B*, and has generally forgotten all about the type A. The only requirement is that A* be convertible to B*. B may derive from A using multiple inheritance. And this implies that the value of the pointer itself may change when converting from A to B and vice-versa. In this example, shared_ptr<B> needs to remember two things:

  1. How to return a B* when get() is called.
  2. How to delete a A* when it is time to do so.

A very nice implementation technique to accomplish this is to store the B* in the shared_ptr object, and the A* within the control block with the reference count.

自由如风2024-12-03 17:16:04

引用计数不能存储在shared_ptr中。 shared_ptr 必须在各个实例之间共享引用计数,因此 shared_ptr 必须有一个指向引用计数的指针。此外,shared_ptrmake_shared 的结果)没有将引用计数存储在分配对象的同一分配中

make_shared 的要点是防止为 shared_ptr 分配两块内存。通常,如果您只执行 shared_ptr(new T()),除了分配的 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函数。

智能指针类型的激增是需要避免的。您需要一个不可移动的、一个可移动的和一个可复制的共享的。还有一个可以打破后者的循环引用。如果您开始拥有多个此类类型,那么您要么有非常特殊的需求,要么您做错了什么。

The reference count cannot be stored in a shared_ptr. shared_ptrs have to share the reference count among the various instances, therefore the shared_ptr must have a pointer to the reference count. Also, shared_ptr (the result of make_shared) does not have to store the reference count in the same allocation that the object was allocated in.

The point of make_shared is to prevent the allocation of two blocks of memory for shared_ptrs. Normally, if you just do shared_ptr<T>(new T()), you have to allocate memory for the reference count in addition to the allocated T. make_shared puts this all in one allocation block, using placement new and delete to create the T. So you only get one memory allocation and one deletion.

But shared_ptr must still have the possibility of storing the reference count in a different block of memory, since using make_shared is not required. Therefore it needs two pointers.

Really though, this shouldn't bother you. Two pointers isn't that much space, even in 64-bit land. You're still getting the important part of intrusive_ptr's functionality (namely, not allocating memory twice).


Your question seems to be "why should make_shared return a shared_ptr instead of some other type?" There are many reasons.

shared_ptr is intended to be a kind of default, catch-all smart pointer. You might use a unique_ptr or scoped_ptr for cases where you're doing something special. Or just for temporary memory allocations at function scope. But shared_ptr is intended to be the sort of thing you use for any serious reference counted work.

Because of that, shared_ptr would be part of an interface. You would have functions that take shared_ptr. You would have functions that return shared_ptr. And so on.

Enter make_shared. Under your idea, this function would return some new kind of object, a make_shared_ptr or whatever. It would have its own equivalent to weak_ptr, a make_weak_ptr. But despite the fact that these two sets of types would share the exact same interface, you could not use them together.

Functions that take a make_shared_ptr could not take a shared_ptr. You might make make_shared_ptr convertible to a shared_ptr, but you couldn't go the other way around. You wouldn't be able to take any shared_ptr and turn it into a make_shared_ptr, because shared_ptr needs to have two pointers. It can't do its job without two pointers.

So now you have two sets of pointers which are half-incompatible. You have one-way conversions; if you have a function that returns a shared_ptr, the user had better be using a shared_ptr instead of a make_shared_ptr.

Doing this for the sake of a pointer's worth of space is simply not worthwhile. Creating this incompatibility, creating two sets of pointers just for 4 bytes? That simply isn't worth the trouble that is caused.

Now, perhaps you would ask, "if you have make_shared_ptr why would you ever need shared_ptr at all?"

Because make_shared_ptr is insufficient. make_shared is not the only way to create a shared_ptr. Maybe I'm working with some C-code. Maybe I'm using SQLite3. sqlite3_open returns a sqlite3*, which is a database connection.

Right now, using the right destructor functor, I can store that sqlite3* in a shared_ptr. That object will be reference counted. I can use weak_ptr where necessary. I can play all the tricks I normally would with a regular C++ shared_ptr that I get from make_shared or whatever other interface. And it would work perfectly.

But if make_shared_ptr exists, then that doesn't work. Because I can't create one of them from that. The sqlite3* has already been allocated; I can't ram it through make_shared, because make_shared constructs an object. It doesn't work with already existing ones.

Oh sure, I could do some hack, where I bundle the sqlite3* in a C++ type who's destructor will destroy it, then use make_shared to create that type. But then using it becomes much more complicated: you have to go through another level of indirection. And you have to go through the trouble of making a type and so forth; the destructor method above at least can use a simple lambda function.

Proliferation of smart pointer types is something to be avoided. You need an immobile one, a movable one, and a copyable shared one. And one more to break circular references from the latter. If you start to have multiple ones of those types, then you either have very special needs or you are doing something wrong.

书信已泛黄2024-12-03 17:16:04

我有一个 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* 您必须首先取消引用控制块,这是不切实际的。

I have a honey::shared_ptr implementation that automatically optimizes to a size of 1 pointer when intrusive. It's conceptually simple -- types that inherit from SharedObj have an embedded control block, so in that case shared_ptr<DerivedSharedObj> is intrusive and can be optimized. It unifies boost::intrusive_ptr with non-intrusive pointers like std::shared_ptr and std::weak_ptr.

This optimization is only possible because I don't support aliasing (see Howard's answer). The result of make_shared can then have 1 pointer size if T is known to be intrusive at compile-time. But what if T is known to be non-intrusive at compile-time? In this case it's impractical to have 1 pointer size as shared_ptr must behave generically to support control blocks allocated both alongside and separately from their objects. With only 1 pointer the generic behavior would be to point to the control block, so to get at T* you'd have to first dereference the control block which is impractical.

陌伤浅笑2024-12-03 17:16:04

其他人已经说过shared_ptr需要两个指针,因为它必须指向引用计数内存块和指向类型内存块。

我想您要问的是:

当使用 make_shared 时,两个内存块都会合并为一个,并且因为块大小和对齐方式是已知的,并且在编译时固定,一个指针可以从另一个指针计算出来(因为它们有固定的偏移量)。那么为什么标准或 boost 不创建第二种类型,例如 small_shared_ptr ,它只包含一个指针。
是这样吗?

答案是,如果你仔细思考,它很快就会变成一个大麻烦,却收效甚微。如何使指针兼容?一个方向,即将 small_shared_ptr 分配给 shared_ptr 会很容易,反之则极其困难。即使您有效地解决了这个问题,您所获得的微小效率也可能会因往返转换而损失,而这种转换将不可避免地出现在任何严肃的程序中。而且额外的指针类型也使得使用它的代码更难理解。

Others have already said that shared_ptr needs two pointers because it has to point to the reference count memory block and the Pointed to Types memory Block.

I guess what you are asking is this:

When using make_shared both memory blocks are merged into one, and because the blocks sizes and alignment are known and fixed at compile time one pointer could be calculated from the other (because they have a fixed offset). So why doesn't the standard or boost create a second type like small_shared_ptr which does only contain one pointer.
Is that about right?

Well the answer is that if you think it through it quickly becomes a large hassle for very little gain. How do you make the pointers compatible? One direction, i.e. assigning a small_shared_ptr to a shared_ptr would be easy, the other way round extremely hard. Even if you solve this problem efficiently, the small efficiency you gain will probably be lost by the to-and-from conversions that will inevitably sprinkle up in any serious program. And the additional pointer type also makes the code that uses it harder to understand.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文