17

Consider the following code:

#include <iostream>
#include <memory>
using namespace std;

class T;

std::weak_ptr<T> wptr;

class T
{
public:
    T() {  }
    ~T() {
        std::cout << "in dtor" << std::endl;
        std::cout << (wptr.expired() ? "expired" : "not expired") << std::endl;
    }
};

int main() {
    {
        auto ptr = std::make_shared<T>();
        wptr = ptr;
        std::cout << (wptr.expired() ? "expired" : "not expired") << std::endl;
    }
    return 0;
}

In this code, I was trying to find out if weak_ptrs are expired in the objects destruction phase. It seems so. The output is:

not expired
in dtor
expired

I used gcc-5.1 with ideone.

Now, I have another problem. I couldn't find any documentation stating that this is the standard behavior. Is it guaranteed to work this way, always?

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
jnbrq -Canberk Sönmez
  • 1,790
  • 1
  • 17
  • 29
  • The standard doesn't specify it explicitly, I think it's an implmentation issue. And from the point of implmentation, decreasing the use_count firstly then destroying the object seems more reasonable; to avoid race condition issue. – songyuanyao Jan 25 '17 at 12:44
  • @songyuanyao: Please write answers in the answer section, so that they can be voted on and edited – Lightness Races in Orbit Jan 25 '17 at 12:45
  • I checked your code with latest g++, clang, and VC++. All have the same behavior. – AMA Jan 25 '17 at 12:48
  • 7
    This is another version of https://timsong-cpp.github.io/lwg-issues/2751 – T.C. Jan 25 '17 at 15:47
  • 3
    Independent of specification, I think it *must* be expired in the destructor. If you were to be able to convert it to a `shared_ptr`, you would have a `shared_ptr` that couldn't prevent the object from dying. – Johannes Schaub - litb Jan 26 '17 at 10:11
  • 1
    @T.C. I don't think it's unreasonable of a program to say `wptr.lock`, while a different thread may or may not be destroying the watched object (it running the object's destructor). That's a major use-case, which they seem to be saying is "unreasonable". It seems that I'm misunderstanding them? – Johannes Schaub - litb Jan 26 '17 at 10:15
  • @JohannesSchaub-litb The DR is clearly discussing something else entirely; I am not sure what is discussed, or even whether what is discussed even relates to C++. (Which is not a rare occurrence with the C++ committee.) – curiousguy Jan 27 '17 at 03:38
  • @curiousguy I'm not so sure about the "something else". It seems to be discussing that the spec doesn't say that prior to calling the deleter by `shared_ptr`'s destructor, it won't drop the weak-ptr reference counts. It seems to me that's what this question comes down to. And my comment above (before I did read the defect-report) about "it must be expired" also is immanent in the defect-report: "because otherwise shared_ptr deleters could resurrect a reference to an object that is being deleted.". – Johannes Schaub - litb Jan 27 '17 at 08:58
  • @JohannesSchaub-litb The DR seems to be discussing destruction of the Deleter object stored into the control block, which is another unrelated issue! – curiousguy Jan 27 '17 at 20:54
  • 1
    @T.C., this is valuable information that is not in one of the answers. Would you like to expand this into an answer? – Carsten S Jan 24 '18 at 11:03
  • **−1** Invalidating an existing answer by changing the question. – Cheers and hth. - Alf Jan 25 '18 at 11:48
  • Anyway you don't need a language-lawyer answer for a guarantee. For that you need only common sense. You would need a language-lawyer answer for the purpose of submitting a Defect Report, but since you have to ask you're not qualified to submit that DR, and should leave that to others. – Cheers and hth. - Alf Jan 25 '18 at 11:51
  • I retagged the question. In case OP did not want a standard-guarantee (imo indicated by putting a bold **always** at its end), there are existing answers: https://stackoverflow.com/questions/14043245/how-does-a-weak-ptr-know-that-the-shared-resources-has-expired, https://stackoverflow.com/questions/32113594/weak-ptr-make-shared-and-memory-deallocation that cover everything from your answer. – midor Jan 25 '18 at 12:26
  • @midor: You have no business changing the meaning of a question. Especially not when that invalidates an existing answer. If you think it's a *possible* duplicate, then just vote to close it as such. – Cheers and hth. - Alf Jan 25 '18 at 12:29
  • **0** Rolled back the change-the-question edit by 3rd party, and removed my downvote. – Cheers and hth. - Alf Jan 25 '18 at 12:29
  • 1
    I did not edit the question text. I retagged it, since it is clearly asking for an answer that gives a guarantee. I know some people who would know more about this will find the question if it is tagged language-lawyer, whereas they won't bother to look through C++. Would prefer clarification from OP himself/herself. – midor Jan 25 '18 at 12:57

4 Answers4

8

Now, I have another problem. I couldn't find any documentation stating that this is the standard behavior. Is it guaranteed to work this way, always?

No. Indeed, it's underspecified in the standard, as raised by LWG issue 2751.

The C++14 standard contains no language that guarantees the deleter run by a shared_ptr will see all associated weak_ptr instances as expired. For example, the standard doesn't appear to guarantee that the assertion in the following snippet won't fire:

std::weak_ptr<Foo> weak;
std::shared_ptr<Foo> strong{
  new Foo,
  [&weak] (Foo* f) {
    assert(weak.expired());
    delete f;
  },
};

weak = strong;
strong.reset();

It seems clear that the intent is that associated weak_ptrs are expired, because otherwise shared_ptr deleters could resurrect a reference to an object that is being deleted.

Suggested fix: 23.11.3.2 [util.smartptr.shared.dest] should specify that the decrease in use_count() caused by the destructor is sequenced before the call to the deleter or the call to delete p.

The current wording for ~shared_ptr() as linked above simply states that the deleter is invoked, with a non-normative note that the number of instances that share ownership is decreased.

While the intent is probably that weak.expired() when the deleter is called, it's questionable to rely on this. It's really only reasonable to state with confidence that the shared_ptr no longer shares ownership after it's been destroyed - asking that question during destruction is a bit odd.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    But if the `shared_ptr` is being destroyed on a different thread, you don't know that you are asking the question _during_ destruction. – Nevin Jan 26 '18 at 19:59
  • @Nevin Yeah but in this case we're in the deleter call. – Barry Jan 26 '18 at 20:14
  • @Nevin Then what question are you asking? Can you even define it? – curiousguy Jan 28 '18 at 11:47
  • 1
    @curiousguy The question `expired()` asks: "would `lock()` have returned an empty `shared_ptr`?" And once it returns `true`, the answer remains `true`; otherwise, the answer is about _what would have happened in the past_ (degenerate case when on the same thread notwithstanding). It gets a bit confusing because in [util.smartptr.weak.obs] `lock()` is defined in terms of `expired()`. Once we are in the deleter `lock()` can no longer return a non-empty `shared_ptr` (because the current object will be destroyed), and `expired()` returns true. In general, only `true` can really be acted upon. – Nevin Jan 29 '18 at 21:14
  • I disagree with the last paragraph. Not only because I have written code that relies on the this ;) Also because it would be difficult to write an implementation that does not guarantee the behaviour in the question but still guarantees that another thread cannot successfully lock() while we are in the destructor. Still, this answer contains the most useful information. Thanks! – Carsten S Jan 30 '18 at 20:02
5

Using make_shared like that will create an object with the default constructor you provided.

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );

Constructs an object of type T and wraps it in a std::shared_ptr using args as the parameter list for the constructor of T. The object is constructed as if by the expression (std::make_shared)

After the anonymous scope in the main. The shared ptr will be deleted.

The object is destroyed and its memory deallocated when either of the following happens:

the last remaining shared_ptr owning the object is destroyed; (std::shared_ptr)

.

The destructor of shared_ptr decrements the number of shared owners of the control block. If that counter reaches zero, the control block calls the destructor of the managed object. The control block does not deallocate itself until the std::weak_ptr counter reaches zero as well. std::shared_ptr Implementation notes

This means that your object will call its destructor after the destruction of the last shared ptr has started. The output:

not expired
in dtor
expired

is the expected behavior.

Petar Velev
  • 2,305
  • 12
  • 24
0

Not the standard itself but:

http://en.cppreference.com/w/cpp/memory/weak_ptr/expired

Checks whether the managed object has already been deleted. Equivalent to use_count() == 0.

So it becomes a question of weather use_count is set to 0 before or after deletion. Now there's a not on this in a draft of the standard: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf [page 566 20.9.2.2.2]

~shared_ptr();

Effects:

  • If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects.
  • Otherwise, if *this owns an object p and a deleter d, d(p) is called.
  • Otherwise, *this owns a pointer p, and delete p is called.

[Note: Since the destruction of *this decreases the number of instances that share ownership with *this by one, after *this has been destroyed all shared_ptr instances that shared ownership with *this will report a use_count()that is one less than its previous value. — end note]

Lanting
  • 3,060
  • 12
  • 28
  • 3
    The note still doesn't specify the order explicitly, isn't it? – songyuanyao Jan 25 '17 at 13:00
  • Arguably the reference count update performed by the destructor *is* a side effect. For if it isn't, then it's magic, and C++ doesn't involve magic. IMHO **the standard is simply wrong here**. And it's not a subtle point. It's as in-your-face unsubtle as it can be. – Cheers and hth. - Alf Jan 25 '18 at 14:07
  • @songyuanyao: The normative text quoted specifies the order: the deleter or a `delete` expression is only called when the `*this` is non-empty and `use_count() == 0`. Or, it would have been a specification of order if the reference count reduction had been specified, but as it is, the quoted part of the standard, effectively maintaining that *nothing happens* because the reference count is never reduced, is just wrong. The note is not normative, and the note does not specify the order. – Cheers and hth. - Alf Jan 25 '18 at 15:16
0

A weak_ptr expires when there are no more shared_ptrs referring to the object.

When (immediately after) the last shared_ptr stops referring to the object it's destroyed.

At this point there are no shared_ptrs referring to it, so any weak_ptr has expired.

The object's destructor is now invoked and if it has separate storage (i.e. wasn't created with make_shared) its storage is deallocated.

The control block, where the reference count and raw pointer and delete function are stored, persists if there are any weak_ptrs referring to it. When the last weak_ptr stops referring to it, also the control block is destroyed and deallocated. I.e., shared_ptr instances keep the object itself alive, along with its control block, while weak_ptr instances keep the control block alive.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • 1
    "_weak_ptr instances keep the control block alive_" IOW, the weak ptr is a strong reference to the meta data – curiousguy Jan 28 '18 at 13:37