In the following code, an X is registered in a global container that becomes a shared owner of it. The destructor of X tests that it is no longer part of such ownership anymore, which I would expect to be a valid precondition for getting destroyed.
#include <vector>
#include <memory>
struct X {
~X();
};
std::vector<std::shared_ptr<X> > global_x_reg;
X::~X()
{
for (auto iter = global_x_reg.begin(), end = global_x_reg.end(); iter != end; ++iter)
if (iter->get() == this)
throw "Oops. X gets destroyed while it is still owned!";
}
int main(int argc, char** argv)
{
global_x_reg.push_back( std::shared_ptr<X>(new X) );
global_x_reg.clear(); // calls X::~X().
}
When it runs (after compilation with VS2010), "Oops..." is thrown when the container is cleared.
Questions:
clear()
in such a way that during the destruction of its values, these values are no longer visible as containees. std::shared_ptr::get
, when std::shared_ptr
is destroying its pointee, return nullptr
?Per N3936 [basic.life]/1: "The lifetime of an object with non-trivial initialization ends when the destructor call starts.", and /3:
The properties ascribed to objects throughout this International Standard apply for a given object only during its lifetime. [ Note: In particular, before the lifetime of an object starts and after its lifetime ends there are significant restrictions on the use of the object, as described below, in 12.6.2 and in 12.7. Also, the behavior of an object under construction and destruction might not be the same as the behavior of an object whose lifetime has started and not ended. 12.6.2 and 12.7 describe the behavior of objects during the construction and destruction phases. —end note ]
You are invoking a member function on a shared_ptr
after the end of its lifetime. Since there's no way to know if a given standard library class member function complies with the restrictions specified, invoking a member function on a standard library object after the end of its lifetime consequently has undefined behavior unless otherwise specified.
See also Library Working Group issue 2382 "Unclear order of container update versus object destruction on removing an object", which pertains very much to the question. In general, it's not a good idea to re-enter a standard library object (global_x_reg
) while it is in the middle of processing a member function call (global_x_reg.clear()
in this case). The class invariants obviously must hold before and after a member call, but there is no guarantee that the object is in a valid state during a call.
I don't think there's any particular reason that .get()
must return NULL
before the deleter (and, consequently, ~X()
) is invoked by std::shared_ptr<X>::~shared_ptr<X>
. The only requirement is that it returns NULL
once std:shared_ptr<X>::~shared_ptr<X>
has completed.
Similarly, if std::vector
is using placement new to construct and destroy its elements (and it will be), there's no reason that it must have updated its accessors before destroying a placed element.
In fact, if you think about this common old-fashioned pattern:
T* ptr = new T();
delete ptr;
ptr = NULL;
It's clear that ~T()
will be invoked before ptr
is set to NULL
; this is pretty much the same thing that you're seeing.
The following extract from libstdc++ v4.6.3 supports the analogy:
00353 virtual void
00354 _M_destroy() // nothrow
00355 {
00356 _My_alloc_type __a(_M_del);
00357 this->~_Sp_counted_deleter();
00358 __a.deallocate(this, 1);
00359 }
(link)
In short, you're observing a totally harmless implementation detail and trying to assert that it breaks stated semantics, when I don't think that it does.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With