C++ destruction order: Calling a field destructor before the class destructor
Asked Answered
C

3

6

Is there any way to call a field destructor before the class destructor?

Suppose I have 2 classes Small and Big, and Big contains an instance of Small as its field as such:

class Small
{
public:
    ~Small() {std::cout << "Small destructor" << std::endl;}
};

class Big
{
public:
    ~Big() {std::cout << "Big destructor" << std::endl;}

private:
    Small small;
};

int main()
{
    Big big;
}

This, of course, calls the big destructor before the small destructor:

Big destructor
Small destructor

I need the Small destructor to be called before the Big destructor since it does some cleanup necessary for the Big destructor.

I could:

  1. call the small.~Small() destructor explicitly. -> This, however, calls the Small destructor twice: once explicitly, and once after the Big destructor has been executed.
  2. have a Small* as the field and call delete small; in the Big destructor

I am aware that I can have a function in the Small class that does the cleanup and call it in the Big destructor, but I was wondering if there was a way to inverse the destructor order.

Is there any better way to do this?

Crowns answered 26/7, 2017 at 15:24 Comment(10)
I don't think there's a better way as the Small destructor will only be called when Big is getting destroyed, thus, after the Big destructor.Leipzig
What kind of clean-up require for small's destructor to be called before big's destructor ?Frontiersman
"I need the small destructor to be called before the big destructor since it does some cleanup necessary for the big destructor." Your design is broken. Ask how to fix it, don't ask how to live with a broken design.Basiliabasilian
"I need the small destructor to be called before the big destructor" - this sounds like an XY problem - why do you need to do this?Bulgarian
Is there any better way to do what? Objects are destroyed in the reverse order as they are created. Anything else is just wrong in most casesContactor
@n.m Yeah, that's broken, but sometimes require.Whole frameworks have such broken design e.g. QtQuick 5.3 hyrbrid app (C++ and jscript) would require something like that on linux, or app leaves hanging process behind itself.Quintillion
I would suggest reworking the design instead of trying to circumvent the standard order of construction/destruction.Deuteranopia
@n.m. Yes, that entered my mind. I guess I don't "need" to do it, I was just unsure if it "could" be done.Crowns
@Frontiersman It's for destroying the zmq context, and the small destructor closes some sockets. The big destructor destroys the zmq context and needs all the sockets to be closed before it can be destroyed.Crowns
@OliverCharlesworth Thank you for that. Reversing the destruction order exactly seems to be Y. I'll see if I can redesign my code. That would be another question, thoughCrowns
G
2

call the small.~Small() destructor explicitly. -> This, however, calls the small destructor twice: once explicitly, and once after the big destructor has been executed.

Well, I don't know why you want to keep on with this flawing design, but you can solve the problem described in your first bullet using placement new.
It follows a minimal, working example:

#include <iostream>

struct Small {
    ~Small() {std::cout << "Small destructor" << std::endl;}
};

struct Big {
    Big() { ::new (storage) Small; }

    ~Big() {
        reinterpret_cast<Small *>(storage)->~Small();
        std::cout << "Big destructor" << std::endl;
    }

    Small & small() {
        return *reinterpret_cast<Small *>(storage);
    }

private:
    unsigned char storage[sizeof(Small)];
};

int main() {
    Big big;
}

You don't have anymore a variable of type Small, but with something like the small member function in the example you can easily work around it.

The idea is that you reserve enough space to construct in-place a Small and then you can invoke its destructor explicitly as you did. It won't be called twice, for all what the Big class has to release is an array of unsigned chars.
Moreover, you won't store your Small into the dynamic storage directly, for actually you are using a data member of your Big to create it in.


That being said, I'd suggest you to allocate it on the dynamic storage unless you have a good reason to do otherwise. Use a std::unique_ptr and reset it at the beginning of the destructor of Big. Your Small will go away before the body of the destructor is actually executed as expected and also in this case the destructor won't be called twice.


EDIT

As suggested in the comments, std::optional can be another viable solution instead of std::unique_ptr. Keep in mind that std::optional is part of the C++17, so if you can use it mostly depends on what's the revision of the standard to which you must adhere.

Geodynamics answered 26/7, 2017 at 16:46 Comment(5)
Yes, I agree with the dynamic storage. That's what I mentioned in '2'. I'm using this approach for now.Crowns
I wonder if a std::optional would be preferable to unique_ptr.Traverse
@ChrisDrew Small isn't optional as far as I understood. The semantics of an optional doesn't fit well here. My two cents.Geodynamics
Personally, I think the semantics of std::optional fits as well as a nullable smart pointer and has the benefit of not using dynamic memory.Traverse
In general, storage might not be correctly aligned for Small. You should use std::aligned_storageCravens
T
2

Without knowing why you want to do this, my only suggestion is to break up Big into the parts that need to be destroyed after Small from the rest and then use composition to include that inside Big. Then you have control over the order of destruction:

class Small
{
public:
    ~Small() {std::cout << "Small destructor" << std::endl;}
};

class BigImpl
{
public:
     ~BigImpl() { std::cout << "Big destructor" << std::endl; }
};

class Big
{
private:
    BigImpl bigimpl;
    Small small;
};
Traverse answered 26/7, 2017 at 15:48 Comment(0)
G
2

call the small.~Small() destructor explicitly. -> This, however, calls the small destructor twice: once explicitly, and once after the big destructor has been executed.

Well, I don't know why you want to keep on with this flawing design, but you can solve the problem described in your first bullet using placement new.
It follows a minimal, working example:

#include <iostream>

struct Small {
    ~Small() {std::cout << "Small destructor" << std::endl;}
};

struct Big {
    Big() { ::new (storage) Small; }

    ~Big() {
        reinterpret_cast<Small *>(storage)->~Small();
        std::cout << "Big destructor" << std::endl;
    }

    Small & small() {
        return *reinterpret_cast<Small *>(storage);
    }

private:
    unsigned char storage[sizeof(Small)];
};

int main() {
    Big big;
}

You don't have anymore a variable of type Small, but with something like the small member function in the example you can easily work around it.

The idea is that you reserve enough space to construct in-place a Small and then you can invoke its destructor explicitly as you did. It won't be called twice, for all what the Big class has to release is an array of unsigned chars.
Moreover, you won't store your Small into the dynamic storage directly, for actually you are using a data member of your Big to create it in.


That being said, I'd suggest you to allocate it on the dynamic storage unless you have a good reason to do otherwise. Use a std::unique_ptr and reset it at the beginning of the destructor of Big. Your Small will go away before the body of the destructor is actually executed as expected and also in this case the destructor won't be called twice.


EDIT

As suggested in the comments, std::optional can be another viable solution instead of std::unique_ptr. Keep in mind that std::optional is part of the C++17, so if you can use it mostly depends on what's the revision of the standard to which you must adhere.

Geodynamics answered 26/7, 2017 at 16:46 Comment(5)
Yes, I agree with the dynamic storage. That's what I mentioned in '2'. I'm using this approach for now.Crowns
I wonder if a std::optional would be preferable to unique_ptr.Traverse
@ChrisDrew Small isn't optional as far as I understood. The semantics of an optional doesn't fit well here. My two cents.Geodynamics
Personally, I think the semantics of std::optional fits as well as a nullable smart pointer and has the benefit of not using dynamic memory.Traverse
In general, storage might not be correctly aligned for Small. You should use std::aligned_storageCravens
C
0

The order of destructor calls cannot be changed. The proper way to design this is that Small performs its own cleanup.

If you cannot change Small then you could make a class SmallWrapper that contains a Small and also can perform the required cleanup.

The standard containers std::optional or std::unique_ptr or std::shared_ptr might suffice for this purpose.

Cravens answered 11/3, 2018 at 23:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.