Why is empty weak_ptr required to store null pointer, while empty shared_ptr is allowed to store non-null pointer?

huangapple go评论77阅读模式
英文:

Why is empty weak_ptr required to store null pointer, while empty shared_ptr is allowed to store non-null pointer?

问题

Naive thinking

我预期对于original参数的每个有效值,以下断言(1)应成立:

#include <memory>
#include <cassert>

void foo(std::shared_ptr<int> original)
{
    std::weak_ptr<int>    weak{original};
    std::shared_ptr<int>  restored{weak.lock()};  // 明确使用lock()以避免异常

    assert( restored == original );         // (1)
}

换句话说,我认为weak_ptr应该能够以非拥有的方式存储shared_ptr的值,然后在锁定时恢复原始值。当然,前提是指向的对象仍然存活。

Reality

显然,我错了,因为以下测试未能通过断言:

void test()
{
    int                       x = 42;
    std::shared_ptr<int>      empty_but_nonnull{std::shared_ptr<char>{}, &x};
    foo(empty_but_nonnull);
}

Wording and test explained

shared_ptr可以拥有一个对象,但存储指向其他对象的指针。
如果shared_ptr不拥有对象,则称其为empty
如果shared_ptr指向没有对象,则称其为null

shared_ptr可以是empty但非null的。标准明确提到并允许这种行为:

[util.smartptr.shared.const]:
17. [Note 2: 此构造函数允许创建一个空的shared_ptr实例,其中存储一个非null的指针。
— end note]

empty但非null的shared_ptr可以被解引用,在条件表达式中隐式转换为true等。标准对此没有提到任何程序被认为是不合法的情况。因此,它似乎是完全合法且可用的。它只是说:“我指向一个不需要任何生命周期管理的对象,可能是全局静态存储期的对象,怎么了?”。

然而,一个empty的weak_ptr不能是非null的。标准明确要求它为null(强调是我的):

[util.smartptr.weak.const]:
4. template<class Y> weak_ptr(const shared_ptr<Y>& r) noexcept;
Effects: 如果r是empty,则构造一个empty的weak_ptr对象,它存储一个null指针值

这意味着从一个empty的shared_ptr构造一个weak_ptr丢失了原始shared_ptr中存储的指针,强制使每个empty的weak_ptr都为null。之后,任何尝试重建原始shared_ptr显然都会失败。

The Question

在引用的规定部分的强调背后的理由是什么?为什么weak_ptr不应该存储与原始shared_ptr相同的指针?

只是因为我最初的天真想法对我来说似乎不那么不合逻辑...

英文:

Naive thinking

I expected the following assertion (1) to hold true for every valid value of original argument:

#include &lt;memory&gt;
#include &lt;cassert&gt;
    
void foo(std::shared_ptr&lt;int&gt; original)
{
    std::weak_ptr&lt;int&gt;    weak{original};
    std::shared_ptr&lt;int&gt;  restored{weak.lock()};  // lock() explicitly to avoid exception

    assert( restored == original );         // (1)
}

In other words, I thought that weak_ptr is supposed to be able to store the value of a shared_ptr in non-owning manner and then restore that original value later, when locked. Assuming the pointed object is still alive, of course.

Reality

Obviously I was wrong, as the following test turned out to fail the assertion:

void test()
{
    int                       x = 42;
    std::shared_ptr&lt;int&gt;      empty_but_nonnull{std::shared_ptr&lt;char&gt;{}, &amp;x};
    foo(empty_but_nonnull);
}

Wording and test explained

shared_ptr can own one object, but store a pointer to some other object.
shared_ptr is called empty if it owns no object.
shared_ptr is called null if it points to no object.

A shared_ptr can be empty, but non-null. Such behavior is explicitly mentioned and allowed by the standard:

> [util.smartptr.shared.const]:
> 17. [Note 2: This constructor allows creation of an empty shared_­ptr instance with a non-null stored pointer.
— end note]

Empty but non-null shared_ptr can be dereferenced, it is implicitly converted to true in conditional expressions, etc. No mention of the program being ill-formed whatsoever. So it seems to be perfectly legit and usable. It just says: "I'm pointing to an object that doesn't require any lifetime management, possibly to a global object with static storage duration, so what?".

However, an empty weak_ptr cannot be non-null. It is explicitly required by the standard to be null (emphasis mine):

> [util.smartptr.weak.const]:
> 4. template&lt;class Y&gt; weak_ptr(const shared_ptr&lt;Y&gt;&amp; r) noexcept;
> Effects: If r is empty, constructs an empty weak_­ptr object that stores a null pointer value

Which means that constructing a weak_ptr from an empty shared_ptr loses the pointer stored in original shared_ptr, forcing every empty weak_ptr to be null. After that, any attempt to reconstruct the original shared_ptr would obviously fail.

The Question

What's the rationale behind the emphasized part of the quoted clause on weak_ptr? Why shouldn't weak_ptr store the same pointer as the original shared_ptr?

It's just that my original naive thinking doesn't seem that illogical to me...


Edit: Example edited to eliminate any possible exception and undefined behavior.

答案1

得分: 2

shared_ptr<T> 持有指向 T 类型的指针,并拥有类型为 U 的某个对象,该对象可能与 T 相同,也可能不同。这个特性存在的目的是允许 shared_ptr<T> 指向其拥有的 U 对象的某个组件。例如,你可以创建一个 shared_ptr<derived_class> 并将其转换为一个 shared_ptr<base_class>,同时仍然拥有一个 derived_class。或者你可以有一个 shared_ptr<some_struct> 并创建一个 shared_ptr<some_structs_member>,它仍然拥有一个 some_struct。能够执行这些操作是这一特性的目的。

请注意,在上述情况中,指向的对象是由所拥有的对象所拥有的。也就是说,如果拥有的 U 对象被销毁,shared_ptr 持有的 T 指针将不再有效。

weak_ptr<T> 不是用于重建 shared_ptr<T> 的工具。它有一个目的:如果仍然存在一个拥有 weak_ptr<T> 弱拥有的内存的 shared_ptr,那么你可以从中提取一个 shared_ptr<T>

weak_ptr<T>::lock 不关心 T 指针(它会保留它,但它不关心它);它只关心拥有权。成功对 weak_ptr 进行锁定意味着被管理的对象仍然存活。而“成功锁定”的唯一定义是 shared_ptr<T> 是否为 nullptr。除了 use_count 之外,一个拥有 U 的非空 shared_ptr<T> 和一个不拥有 U 的非空 shared_ptr<T> 之间没有可观察的区别。

请记住:存储的 T 应该是由 U 所拥有的对象。因此,如果 weak_ptr<T> 打算弱拥有 shared_ptr<T>,并发现 shared_ptr<T> 不拥有任何东西... 那么它会假定 T 也已被销毁。因为这就是这个特性的目的

现在,shared_ptr<T>shared_ptr<U> 的构造函数是否也可以检查给定的 shared_ptr<U> 是否实际拥有某些内容,并在这种情况下抛出异常?它可以,但 C++ 不是一种安全的语言。此外,shared_ptr<T> 构造函数发出的唯一异常(除了实现定义的异常)是由于内存分配失败或无法锁定 weak_ptr<T> 而引起的。

因此,总的来说,标准假定你在创建 shared_ptr 时知道自己在做什么。

英文:

shared_ptr&lt;T&gt; hold a pointer to a T and owns some object of type U which may or may not be the same as T. This feature exist to allow the shared_ptr&lt;T&gt; to point to some object that is a component of the U object that it owns. For example, you can create a shared_ptr&lt;derived_class&gt; and convert it to a shared_ptr&lt;base_class&gt; while still owning a derived_class. Or you can have a shared_ptr&lt;some_struct&gt; and create a shared_ptr&lt;some_structs_member&gt; which still owns a some_struct. Being able to do these things is the feature's purpose.

You will note that in the above cases, the object being pointed to is owned by the object being owned. That is, if the owned U object is destroyed, the T pointer held by the shared_ptr is no longer valid.

weak_ptr&lt;T&gt; is not a tool that is meant to be used to reconstitute a shared_ptr&lt;T&gt;. It is a tool that has one purpose: if a shared_ptr still exists which owns the memory weakly owned by the weak_ptr&lt;T&gt;, then you may extract a shared_ptr&lt;T&gt; from it.

weak_ptr&lt;T&gt;::lock does not care about the T pointer (it does preserve it, but it doesn't care about it); it only cares about ownership. Successfully locking a weak_ptr means that the object being managed is still alive. And the only definition of "successfully locking" is whether the shared_ptr&lt;T&gt; is nullptr. With the exception of use_count there is no observable difference between a non-null shared_ptr&lt;T&gt; that owns a U and a non-null shared_ptr&lt;T&gt; that doesn't own a U.

Remember: the expectation is that the T being stored is an object that is owned by U. Therefore, if a weak_ptr&lt;T&gt; is going to claim weak ownership over a shared_ptr&lt;T&gt;, and it finds that the shared_ptr&lt;T&gt; does not own anything... it assumes that the T is also destroyed. Because that's what the feature is for.

Now, could the constructor of shared_ptr&lt;T&gt; from a shared_ptr&lt;U&gt; also check to see if the shared_ptr&lt;U&gt; it is given actually owns something and throw an exception in that case? It could, but C++ is not a safe language. Furthermore, the only exceptions shared_ptr&lt;T&gt; constructors emit (other than implementation-defined ones) are from failure to allocate memory or failure to lock a weak_ptr&lt;T&gt;.

So at the end of the day, the standard assumes you know what you're doing when creating the shared_ptr.

huangapple
  • 本文由 发表于 2023年2月16日 17:09:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/75469966.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定