共享指针<>是不是需要使用引用计数?
我是否理解新标准,即 不需要使用引用计数shared_ptr
?只是很可能是这样实现的?
我可以想象一个以某种方式使用隐藏链表的实现。在N3291“20.7.2.2.5.(8)shared_ptr观察者[util.smartptr.shared.obs]”中,注释说
[ 注意:use_count() 不一定高效。 ——尾注]
这给了我这个想法。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
你是对的,规范中没有任何内容要求使用显式的“计数器”,并且存在其他可能性。
例如,建议实施链表实现shared_ptr 的 a>;然而,该提案最终被拒绝,因为它引入了其他领域的成本(大小、复制操作和线程安全)。
You're right, nothing in the spec requires the use of an explicit "counter", and other possibilities exist.
For example, a linked-list implementation was suggested for the implementation of boost's
shared_ptr
; however, the proposal was ultimately rejected because it introduced costs in other areas (size, copy operations, and thread safety).摘要描述
有人说
shared_ptr
是一个“引用计数器智能指针”。我认为这不是正确的看待方式。实际上,
shared_ptr
都是关于(非独占)所有权的:所有shared_ptr
都是已初始化的shared_ptr
的副本带有指针p
的是所有者。shared_ptr
跟踪所有者集合,以保证:delete p
所有者集合变空,立即调用delete p
(或销毁函子D
的副本);当然,要确定所有者集合何时变空,
shared_ptr
只需要一个计数器。抽象的描述稍微容易思考一些。可能的实现技术
为了跟踪所有者的数量,计数器不仅是最明显的方法,而且如何使用原子比较和修改来实现线程安全也是相对明显的。
为了跟踪所有所有者,所有者链接列表不仅是显而易见的解决方案,而且还是避免为每组所有者分配任何内存的简单方法。问题在于,要有效使这种方法线程安全并不容易(任何东西都可以通过全局锁来实现线程安全,这违背了并行性的理念)。
在多线程实现的情况下
,一方面,我们有一个小的、固定大小的(除非使用自定义销毁函数)内存分配,这很容易优化,并且有简单的整数原子操作。
另一方面,链表处理成本高昂且复杂;如果需要每个所有者设置互斥体(正如我认为的那样),则内存分配的成本又回来了,此时我们可以用计数器替换互斥体!
关于多种可能的实现
我读过多少次“标准”类可以有多种实现?
谁从未听说过这种幻想:可以将复杂的类实现为极坐标?众所周知,这是愚蠢的。复杂必须使用笛卡尔坐标。如果首选极坐标,则必须创建另一个类。极性复杂类不可能用作通常复杂类的直接替代品。
对于(非标准)字符串类也是如此:字符串类没有理由在内部以 NUL 终止并且不将长度存储为整数,只是为了重复调用 strlen 的乐趣和低效率。
我们现在知道,设计 std::string 来容忍 COW 是一个坏主意,这就是 const 迭代器异常失效语义的原因。
std::vector
现在保证是连续的。幻想的终结
在某些时候,标准类具有许多显着不同的合理实现的幻想必须被放弃。标准类是原始构建块;它们不仅应该非常高效,而且它们应该具有可预测的效率。
程序员应该能够对基本操作的相对速度做出可移植的假设。如果即使是最简单的加法也会变成一堆超越计算,那么复杂的类对于严肃的数字运算来说是无用的。如果不能保证字符串类通过数据共享进行非常快速的复制,则程序员将不得不最小化字符串复制。
实施者可以自由选择不同的实施技术仅当它不会使常见的廉价操作变得极其昂贵(相比之下)。
对于许多类来说,这意味着只有一种可行的实现策略,有时具有一定程度的自由度(例如 std::deque 中块的大小)。
Abstract description
Some people say that
shared_ptr
is a "reference counter smart pointer". I don't think it is the right way to look at it.Actually
shared_ptr
is all about (non-exclusive) ownership: all theshared_ptr
that are copies of ashared_ptr
initialised with a pointerp
are owners.shared_ptr
keeps track of the set of owners, to guaranty that:delete p
is not calleddelete p
(or a copy ofD
the destruction functor) is called immediately;Of course, to determine when the set of owners becomes empty,
shared_ptr
only needs a counter. The abstract description is just slightly easier to think about.Possible implementations techniques
To keep track of the number of owners, a counter is not only the most obvious approach, it's also relatively obvious how to make thread-safe using atomic compare-and-modify.
To keep track all the owners, a linked list of owner is not only the obvious solution, but also an easy way to avoid the need to allocate any memory for each set of owners. The problem is that it isn't easy to make such approach efficiently thread safe (anything can be made thread safe with a global lock, which is against the very idea of parallelism).
In the case of multi-thread implementation
On the one hand, we have a small, fix-size (unless the custom destruction function is used) memory allocation, that's very easy to optimise, and simple integer atomic operations.
On the other hand, there is costly and complicated linked-list handling; and if a per owners set mutex is needed (as I think it is), the cost of memory allocation is back, at which point we can just replace the mutex with the counter!
About multiple possible implementations
How many times I have read that many implementations are possible for a "standard" class?
Who has never heard this fantasy that the complex class that could be implemented as polar coordinates? This is idiotic, as we all know. complex must use Cartesian coordinates. In case polar coordinates are preferred, another class must be created. There is no way a polar complex class is going to be used as a drop-in replacement for the usual complex class.
Same for a (non-standard) string class: there is no reason for a string class to be internally NUL terminated and not store the length as an integer, just for the fun and inefficiency of repeatedly calling
strlen
.We now know that designing
std::string
to tolerate COW was a bad idea that is the reason for the unusual invalidation semantics of const iterators.std::vector
is now guaranteed to be continuous.The end of the fantasy
At some point, the fantasy where standard classes have many significantly different reasonable implementations has to be dropped. Standard classes are primitive building blocks; not only they should be very efficient, they should have predictable efficiency.
A programmer should be able to make portable assumptions about the relative speed of basic operations. A complex class is useless for serious number crunching if even the simplest addition turns into a bunch a transcendental computations. If a string class is not guaranteed to have very fast copy via data sharing, the programmer will have to minimize string copies.
An implementer is free to choose a different implementation techniques only when it doesn't make a common cheap operation extremely costly (by comparison).
For many classes, this means that there is exactly one viable implementation strategy, with sometimes a few degrees of liberty (like the size of a block in a
std::deque
).