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?

Humam Helfawi
  • 19,566
  • 15
  • 85
  • 160
ksb
  • 670
  • 8
  • 19
  • Why do you need to access A from the destructor of B? – Rick de Water Jun 22 '16 at 06:27
  • 1
    Please clarify what you mean by "owns an object B"? It owns it via a smart pointer? – Jonathan Wakely Jun 22 '16 at 06:27
  • Yeah say A has a std::unique_ptr. – ksb Jun 22 '16 at 06:30
  • general best practice is to avoid cyclical references. Try shared_ptr to break it. – Incomputable Jun 22 '16 at 06:36
  • 2
    Its not a cyclic reference. A owns B, B does not own A. – ksb Jun 22 '16 at 06:36
  • so, the question comes to is it safe to access members of the object being destructed outside of the destructor? – Incomputable Jun 22 '16 at 06:44
  • 1
    @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_ptr` – PcAF Jun 22 '16 at 07:03
  • Why do you have destructirs at all? Most objects don't need destructors. – n. m. could be an AI Jun 22 '16 at 08:33
  • No matter how you do this you will have very brittle code that is an invitation to disaster. It's not a good idea. – Cheers and hth. - Alf Jun 22 '16 at 08:55
  • 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](http://melpon.org/wandbox/permlink/D4kGFrUJm5QiskPe). – Chris Drew Jun 22 '16 at 10:10
  • Can you please explain what you mean by *"so that B is destroyed in a predictable manner"*. – sameerkn Jun 22 '16 at 10:15

5 Answers5

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.

Community
  • 1
  • 1
Chris Drew
  • 14,926
  • 3
  • 34
  • 54
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.

Community
  • 1
  • 1
PcAF
  • 1,975
  • 12
  • 20
  • What's a safe way to detect in the destructor of B, that we should not access A? – ksb Jun 22 '16 at 07:59
  • 3
    Is it really this straightforward? Apparently, [it is ok to use `this` during a destructor](http://stackoverflow.com/questions/10979250/usage-of-this-in-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. – Chris Drew Jun 22 '16 at 08:06
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.

iolo
  • 1,090
  • 1
  • 9
  • 20
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.

sameerkn
  • 2,209
  • 1
  • 12
  • 13
  • @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. – sameerkn Jun 22 '16 at 09:47
  • 1
    I think you are wrong. The destructor of B starts after A. But the destructor of B completes before the destructor of A completes. – ksb Jun 22 '16 at 12:29
  • @ksb: If destructor of `B` starts **after** `A` then how can destructor of `B` get completed before `A`? – sameerkn Jun 22 '16 at 12:39
  • If a function A calls a function B, the function B starts after start of A, but completes before completion of A, right? – ksb Jun 22 '16 at 12:41
  • @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`. – sameerkn Jun 22 '16 at 12:47
  • 1
    "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. – ksb Jun 22 '16 at 12:53
  • 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." – sameerkn Jun 22 '16 at 13:05
  • @ksb: If `A`'s destructor invokes `B`'s destructor then in my example `~b()` should have been displayed before `~a()`. – sameerkn Jun 22 '16 at 13:15
  • 1
    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. – ksb Jun 22 '16 at 13:54
  • @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. – sameerkn Jun 23 '16 at 05:57
  • 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. – ksb Jun 23 '16 at 11:03
  • 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. – sameerkn Jun 23 '16 at 12:05
-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.

Roland
  • 336
  • 2
  • 8