如代码here所示,从make_shared返回的对象的大小是两个指针。
但是,为什么make_shared
不像下面这样工作(假设T是我们正在制作共享指针的类型):
make_shared
的结果是一个指针的大小,指向大小为sizeof(int) + sizeof(T)
的已分配内存,其中int是引用计数,并且这会增加并且减少指针的构造/破坏。
unique_ptr
只是一个指针的大小,所以我不确定为什么共享指针需要两个。据我所知,它需要一个引用计数,make_shared
可以与对象本身一起放置。
此外,是否有任何实现以我建议的方式实现(不必为特定对象使用intrusive_ptr
s)?如果没有,我建议实施的原因是什么原因可以避免?
答案 0 :(得分:37)
在我所知的所有实现中,shared_ptr
将拥有的指针和引用计数存储在同一个内存块中。这与其他答案所说的相反。此外,指针的副本将存储在shared_ptr
对象中。 N1431描述了典型的内存布局。
确实可以构建一个引用计数指针,其大小只有一个指针。但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
,通常不是p
。 shared_ptr
中的另一个指针(get()
返回的指针)将为p
。
这被称为别名支持,并在N2351中引入。您可能会注意到shared_ptr
在引入此功能之前有两个指针的大小。在引入此功能之前,可能已经使用一个指针的大小实现了shared_ptr
,但没有人这样做,因为它是不切实际的。在N2351之后,它变得不可能。
N2351之前不切实际的原因之一是支持:
shared_ptr<B> p(new A);
此处,p.get()
会返回B*
,并且通常会忘记A
类型的所有内容。唯一的要求是A*
可以转换为B*
。 B
可以使用多重继承从A
派生。这意味着从A
转换为B
时指针本身的值可能会发生变化,反之亦然。在此示例中,shared_ptr<B>
需要记住两件事:
B*
被调用时返回get()
。A*
时间。 实现此目的的一个非常好的实现技术是将B*
存储在shared_ptr
对象中,并将A*
存储在控制块中,并带有引用计数。
答案 1 :(得分:2)
引用计数不能存储在shared_ptr
中。 shared_ptr
必须在各种实例之间共享引用计数,因此shared_ptr
必须具有指向引用计数的指针。此外,shared_ptr
(make_shared
的结果)不会 将引用计数存储在分配对象的相同分配中。
make_shared
的要点是阻止为shared_ptr
s分配两块内存。通常,如果只执行shared_ptr<T>(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
。我可以使用我从shared_ptr
或其他任何接口获得的常规C ++ make_shared
来播放我通常会使用的所有技巧。它会很完美。
但是如果make_shared_ptr
存在,那么这不起作用。因为我不能从中创建其中一个。 sqlite3*
已经分配;我无法通过make_shared
进行调整,因为make_shared
构造了一个对象。它不适用于现有的。
哦,当然,我可以做一些黑客攻击,我将sqlite3*
捆绑在C ++类型中,析构函数会将其销毁,然后使用make_shared
创建该类型。但随后使用它变得更加复杂:你必须经历另一层次的间接。你必须经历制造类型的麻烦等等;上面的析构函数方法至少可以使用简单的lambda函数。
要避免智能指针类型的扩散。你需要一个固定的,一个可移动的,一个可复制的共享的。还有一个打破后者的循环引用。如果你开始有这些类型的多个,那么你要么有非常特殊的需要,要么你做错了。
答案 2 :(得分:2)
我有一个honey::shared_ptr
实现,在侵入时自动优化为1指针的大小。它在概念上很简单 - 从SharedObj
继承的类型具有嵌入式控制块,因此在这种情况下shared_ptr<DerivedSharedObj>
是侵入式的并且可以进行优化。它将boost::intrusive_ptr
与std::shared_ptr
和std::weak_ptr
等非侵入式指针统一起来。
这种优化是唯一可行的,因为我不支持别名(参见Howard的回答)。如果已知make_shared
在编译时具有侵入性,则T
的结果可以具有1个指针大小。但是如果已知T
在编译时是非侵入式的呢?在这种情况下,将shared_ptr
作为一个指针大小必须通常表现为支持在其对象旁边和与其对象分配的控制块是不切实际的。只有1个指针,通用行为将指向控制块,因此要获得T*
,您必须首先取消引用控制块,这是不切实际的。
答案 3 :(得分:1)
其他人已经说shared_ptr
需要两个指针,因为它必须指向引用计数内存块和指向类型内存块。
我想你问的是这个:
当使用make_shared
时,两个内存块合并为一个,并且因为块大小和对齐是已知的并且在编译时是固定的,所以可以从另一个指针计算一个指针(因为它们具有固定的偏移量)。那么为什么标准或者boost不创建像small_shared_ptr
那样只包含一个指针的第二种类型。
那是对的吗?
答案是答案是,如果你认为通过它很快就会变得非常麻烦,收获很少。你如何使指针兼容?一个方向,即将small_shared_ptr
分配给shared_ptr
将很容易,反过来非常困难。即使你有效地解决了这个问题,你获得的小效率也可能会因为在任何严肃的程序中不可避免地出现的来回转换而丢失。附加的指针类型也使得使用它的代码更难理解。