What is the state of an object once it has entered its destructor?
Asked Answered
D

1

6

Here is some logic:

A::~A() {
  b.foo(this)
}
void B::foo(A*) {
  .. try to get lock ..
  .. remove the A object from some state ...
}
void B::bar() {
   ... try to get lock ...
   ... do some work on the A objects ...
}

This isn't a design question. The above could potentially be done differently.

Suppose that we have some thread of execution that has called bar(). One of the A objects goes out of scope and its destructor is called. Is it valid to continue to make use of that A object in bar(), knowing that it's destructor would attempt to get the same lock that you hold?

In other words, is it valid to continue to use an object during its destructor?

I read several threads and the answer was inconclusive.

Diecious answered 2/6 at 18:10 Comment(11)
The foo function removes the relevant A object from the state that bar accesses @user17732522Diecious
Ah ok, I misunderstood that.Flavourful
As for the answer: Probably it is supposed to be undefined behavior, but I think the standard isn't particularly clear on this and lacks some concept for "lifetime races". For a simple example of what can go wrong in practice: If A has a virtual member function, then, while the destructor runs, it should behave as if it wasn't virtual. However, to achieve that in a typical implementation the vtable ptr must be overwritten at the beginning of the destructor call. That overwrite is unsynchronized with your use in B::bar() and a race. Tools like thread sanitizer will tell you that, too.Flavourful
The A object is an A object until the end of the destructor's body. If the A destructor does anything interesting, to itself, it may have compromised its invariants (or not, if you are careful). At the end of the destructor, it's member variables are destructed, and then it's base classes are destructed. If the instance was a derived-from-A, by the time it gets to the A destructor the derived from destructor has already destructed the derived.Hauteloire
"I read several threads and the answer was inconclusive.": You should link these threads. But as I suggested in my previous comment, the reason you read inconclusive answers is probably that it isn't particularly clearly specified. It certainly does not generally have well-defined behavior.Flavourful
@Hauteloire An object always remains an object. However, the lifetime of the object ends when the destructor call starts. That limits what may be done with the object. As I explained in my previous comment, at the very least calling a virtual member function in B::bar (and other similar operations) must cause undefined behavior and I suspect that the other cases currently are underspecified.Flavourful
@user17732522: [class.ctdor]/4 explicitly allows even virtual calls during destruction; you’re right that the contradiction with [basic.life]/1.4 is profound.Catalpa
@DavisHerring Yes, but surely there is supposed to be something analogues to a data race if the virtual call isn't synchronized with the beginning of the destructor call, exactly because virtual calls in the destructor should behave like a normal call. If an implementation was forced to make this work, then all virtual table pointers would need to be implemented atomically.Flavourful
@user17732522: They don't exactly become a normal call (since one could still call through a base pointer and call the derived function), but you're right in general.Catalpa
How you at all use A *pa in B::bar ? How you ensured that pa is valid during you use it, and you not try use it after pa delete at all ? I be say code/description incomplete for answerMount
This is just bad-practice. If an object is shared among multiple threads, its lifetime must exceed all the threads or all threads must revoke their query to use it before it dies. The object must normally either be static or a shared_ptr. Don't send reference_wraper instances to threads and don't capture references in lambdas that threads run.Ferryboat
T
0

The object lifetime ended when the destructor started. You can still use the object but the behavior of the object which life-time is ended can be different (11.9.5).

There are several things that are UB when using an object which life time is ended. Such as trying to access already destroyed sub-objects or trying to access a non-static members after destruction. One of the possible way compiler can deal with a code containing UB is optimizing it away. So if a compiler can prove that some branch of execution ends in UB it can remove all that code altogether.

Thanatopsis answered 3/6 at 1:0 Comment(3)
Can you provide some examples of what you can't do in the destructor body? The subobjects are not destroyed yet, and member functions can still be called, so to me it seems that it doesn't really matter that object's lifetime technically ended.Biel
HolyBlackCat -- you can do anything, but the result can be different than in normal member function. For example, if you set some fields in destructor, compiler can optimize that code away, because you will never be able to observe them (without UB) after destructor. I debugged code that set the first integer field of the object to zero in destructor, and the other code inspected the storage to determine if the object was destroyed. Compiler optimized that code away.Thanatopsis
This is annoying, but doesn't apply in OP's case, from what I see. They just call a function on an object in its destructor, they don't read the object after leaving the destructor body.Biel

© 2022 - 2024 — McMap. All rights reserved.