Accessing owner in destructor c++
Asked Answered
S

5

9

Say there is an object A which owns an object B via std::unique_ptr<B>. Further B holds a raw pointer(weak) reference to A. Then the destructor of A will invoke the destructor of B, since it owns it.

What will be a safe way to access A in the destructor of B? (since we may also be in the destructor of A).

A safe way me be to explicitly reset the strong reference to B in the destructor of A, so that B is destroyed in a predictable manner, but what's the general best practice?

Splendor answered 22/6, 2016 at 6:22 Comment(11)
Why do you need to access A from the destructor of B?Avraham
Please clarify what you mean by "owns an object B"? It owns it via a smart pointer?Nepos
Yeah say A has a std::unique_ptr<B>.Splendor
general best practice is to avoid cyclical references. Try shared_ptr to break it.Endamage
Its not a cyclic reference. A owns B, B does not own A.Splendor
so, the question comes to is it safe to access members of the object being destructed outside of the destructor?Endamage
@OlzhasZhumabek shared_ptr won't help, it's destructor will be still invoked 2 times: A destructor -> B destructor -> shared_ptr(to A) destructor -> A destructor -> B destructor -> shared_ptr destructor = double destructor = Undefined Behaviour, didn't you mean weak_ptrBrunei
Why do you have destructirs at all? Most objects don't need destructors.Inheritor
No matter how you do this you will have very brittle code that is an invitation to disaster. It's not a good idea.Travail
Perhaps you could make it less fragile by extracting out a class C from A containing the parts that B has a dependency on? Then A can ensure that B is destroyed before C and that B can't access any parts of A that it shouldn't like this.Vermont
Can you please explain what you mean by "so that B is destroyed in a predictable manner".Inebriant
V
4

I'm no language lawyer but I think it is OK. You are treading on dangerous ground and perhaps should rethink your design but if you are careful I think you can just rely on the fact that members are destructed in the reverse order they were declared.

So this is OK

#include <iostream>

struct Noisy {
    int i;
    ~Noisy() { std::cout << "Noisy " << i << " dies!" << "\n"; }
};

struct A;

struct B {
    A* parent;
    ~B();
    B(A& a) : parent(&a) {}
};

struct A {
    Noisy n1 = {1};
    B     b;
    Noisy n2 = {2};
    A() : b(*this) {}
};

B::~B() { std::cout << "B dies. parent->n1.i=" << parent->n1.i << "\n"; }

int main() {
    A a;
}

Live demo.

since the members of A are destructed in order n2 then b then n1. But this is not OK

#include <iostream>

struct Noisy {
    int i;
    ~Noisy() { std::cout << "Noisy " << i << " dies!" << "\n"; }
};

struct A;

struct B {
    A* parent;
    ~B();
    B(A& a) : parent(&a) {}
};

struct A {
    Noisy n1 = {1};
    B     b;
    Noisy n2 = {2};
    A() : b(*this) {}
};

B::~B() { std::cout << "B dies. parent->n2.i=" << parent->n2.i << "\n"; }

int main() {
    A a;
}

Live demo.

since n2 has already been destroyed by the time B tries to use it.

Vermont answered 22/6, 2016 at 8:51 Comment(0)
B
3

What will be a safe way to access A in the destructor of B? (since we may also be in the destructor of A).

There isn't safe way:

3.8/1

[...]The lifetime of an object of type T ends when:

— if T is a class type with a non-trivial destructor (12.4), the destructor call starts [...]

I think it's straightforward that you can't access object after it's lifetime has ended.

EDIT: As Chris Drew wrote in comment you can use object after it's destructor started, sorry, my mistake I missed out one important sentence in the standard:

3.8/5

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that refers to the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see 12.7. Otherwise, such a pointer refers to allocated storage (3.7.4.2), and using the pointer as if the pointer were of type void*, is well-defined. Such a pointer may be dereferenced but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if: [...]

In 12.7 there is list of things you can do during construction and destruction, some of the most important:

12.7/3:

To explicitly or implicitly convert a pointer (a glvalue) referring to an object of class X to a pointer (reference) to a direct or indirect base class B of X, the construction of X and the construction of all of its direct or indirect bases that directly or indirectly derive from B shall have started and the destruction of these classes shall not have completed, otherwise the conversion results in undefined behavior. To form a pointer to (or access the value of) a direct non-static member of an object obj, the construction of obj shall have started and its destruction shall not have completed, otherwise the computation of the pointer value (or accessing the member value) results in undefined behavior.

12.7/4

Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2). When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class’s non-static data members, and the object to which the call applies is the object (call it x) under construction or destruction, the function called is the final overrider in the constructor’s or destructor’s class and not one overriding it in a more-derived class. If the virtual function call uses an explicit class member access (5.2.5) and the object expression refers to the complete object of x or one of that object’s base class subobjects but not x or one of its base class subobjects, the behavior is undefined.

Brunei answered 22/6, 2016 at 7:12 Comment(2)
What's a safe way to detect in the destructor of B, that we should not access A?Splendor
Is it really this straightforward? Apparently, it is ok to use this during a destructor. So I assume it is ok to call a function that uses this during a destructor so why is it not ok for a member to use a pointer to the parent during a destructor? Sure, you have to be careful what other members you access.Vermont
B
0

As has already been mentioned there is no "safe way". In fact as has been pointed out by PcAF the lifetime of A has already ended by the time you reach B's destructor.
I also just want to point out that this is actually a good thing! There has to be a strict order in which objects get destroyed.
Now what you should do is tell B beforehand that A is about to get destructed.
It is as simple as

void ~A( void ) {
    b->detach_from_me_i_am_about_to_get_destructed( this );
}

Passing the this pointer might be necessary or not depending on the design ob B (If B holds many references, it might need to know which one to detach. If it only holds one, the this pointer is superfluous).
Just make sure that appropriate member functions are private, so that the interface only can be used in the intended way.

Remark: This is a simple light-weight solution that is fine if you yourself completely control the communication between A and B. Do not under any circumstances design this to be a network protocol! That will require a lot more safety fences.

Bergschrund answered 22/6, 2016 at 8:21 Comment(0)
I
0

Consider this:

struct b
{
        b()
        {
                cout << "b()" << endl;
        }
        ~b()
        {
                cout << "~b()" << endl;
        }
};

struct a
{
        b ob;
        a()
        {
                cout << "a()" << endl;
        }
        ~a()
        {
                cout << "~a()" << endl;
        }
};

int main()
{
        a  oa;
}

//Output:
b()
a()
~a()
~b()

"Then the destructor of A will invoke the destructor of B, since it owns it." This is not the correct way of invocation of destructors in case of composite objects. If you see above example then, first a gets destroyed and then b gets destroyed. a's destructor won't invoke b's destructor so that the control would return back to a's destructor.

"What will be a safe way to access A in the destructor of B?". As per above example a is already destroyed therefore a cannot be accessed in b's destructor.

"since we may also be in the destructor of A).". This is not correct. Again, when the control goes out of a's destructor then only control enters b's destructor.

Destructor is a member-function of a class T. Once the control goes out of destructor, the class T cannot be accessed. All the data-members of class T can be accessed in class T's constructors and destructor as per above example.

Inebriant answered 22/6, 2016 at 8:26 Comment(12)
@ChrisDrew: Have answered [1] whether its safe to access A in destructor of B whose answer is No. [2] safe way to explicitly reset the strong reference to B in the destructor of A then it can always be done since data members of class can be accessed in the destructor.Inebriant
I think you are wrong. The destructor of B starts after A. But the destructor of B completes before the destructor of A completes.Splendor
@ksb: If destructor of B starts after A then how can destructor of B get completed before A?Inebriant
If a function A calls a function B, the function B starts after start of A, but completes before completion of A, right?Splendor
@ksb: Correct, but how can B's destructor call A's destructor. Are you trying to delete A inside B's destructor? A owns B i.e B is data member of A so B's destructor will get completed first unless you try deleting A from B.Inebriant
"A owns B i.e B is data member of A so B's destructor will get completed first" This is correct and is the same as what I said. But in your answer you wrote "If you see above example then, first a gets destroyed and then b gets destroyed. a's destructor won't invoke b's destructor so that the control would return back to a's destructor." Internally a's destructor does invoke b's destructor and control reaches back to a's destructor.Splendor
Had a mistake in my previous comment. It should be "A owns B i.e B is data member of A so A's destructor will get completed first". Also, can you please show how "Internally a's destructor does invoke b's destructor and control reaches back to a's destructor."Inebriant
@ksb: If A's destructor invokes B's destructor then in my example ~b() should have been displayed before ~a().Inebriant
I had used the word internally. Compiler also adds code which calls the base class destructor and destructor for all member objects. I consider that to be also part of the destructor itself. If we manually call ~Foo(), the member objects of Foo are also destructed.Splendor
@ksb: In any case the code which you are talking about is called thunk and such thunk are added by complier at the end in case of d'tor and at the start in case of c'tor. So once the code written by you in d'tor gets executed then such thunk will executed and after executing thunk the ending **} of d'tor will be encountered. So, you can access B inside A's d'tor but ideally A is theoratically destroyed by the time you reach B's d'tor.Inebriant
Oh ok. I din't know that. Online references for thunk are related to virtual calls. Any reference you can point to? Also I still don't think A is destroyed by the time we reach B's dtor.Splendor
For thunk related: the book is Inside the C++ Object Model. ideally A is theoretically destroyed by the time you reach B's d'tor. A is theoretically destroyed means all the code inside A'a d'tor got executed. Data member can access other data member in the destruction process of a class object depending upon their order in class, as specified by @Chris Drew.Inebriant
G
-2

If you look only on the relations of the two classes A and B, the construction is well:

class A {
    B son;
    A(): B(this)  {}
};   
class B {
    A* parent;
    B(A* myparent): parent(myparent)  {} 
    ~B() {
        // do not use parent->... because parent's lifetime may be over
        parent = NULL;    // always safe
    }
}

The problems arise, if objects of A and B are proliferated to other program units. Then you should use the tools from std::memory like std::shared_ptr or std:weak_ptr.

Gendron answered 22/6, 2016 at 7:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.