C++ Logo

std-proposals

Advanced search

[std-proposals] Call virtual method from base's destructor

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Wed, 29 May 2024 11:38:14 +0100
I wrote a class to manage a COM port on a microcontroller (i.e. RS232
traffic). The destructor didn't need to do anything so I started out
with:

    class IRS232 {
    public:
        virtual ~RS232(void) noexcept {}
    };

    class RS232 : public IRS232 {};

Later I had to write an emulator/simulator to run the firmware on a
desktop PC, and so now I had to close the COM port upon destruction. I
wanted to put the 'Close' operation inside the base class rather than
have to put it in all the derived classes (e.g. RS232_Microcontroller,
RS232_Win32, RS232_Boost, etc). So I started out with:

    class IRS232 {
    public:
        virtual void Close(void) noexcept = 0;
        virtual ~RS232(void) noexcept { this->Close(); }
    };

Of course, the problem here is that if we make another class such as
"RS232_Win32" and derive it from IRS232, the RS232_Win32 part of the
object has already been destroyed by the time the base class's
destructor is called, and so the invocation of the 'Close' method
might try to access an object that is either in an invalid state or no
longer exists.

Of course what needs to be done here, is that the derived classes
(e.g. "RS232_Win32" and "RS232_Boost"), must invoke the 'Close' method
inside their own destructors. To spell it out:

    RS232_Microcontroller::~RS232_Microcontroller must call "this->Close()"
    RS232_Win32::~RS232_Win32 must call "this->Close()"
    RS232_Boost::~RS232_Boost must call "this->Close()"

Still though . . . I feel like the base class should have something
written between its two curly braces to do either one of two things:
    (1) Inform the programmer that their derived class should close
the COM port upon destruction
    (2) Compel the programmer to make their derived class close the
COM port upon destruction

No. 1 can be achieved with a simple comment as follows:

    class IRS232 {
    public:
        virtual void Close(void) noexcept = 0;
        virtual ~RS232(void) noexcept
        {
            // Make sure all derived classes
            // close the COM port in their
            // own destructors, i.e.
            // this->Close();
        }
    };

I was thinking that No.2 could be achieved something like:

    class IRS232 {
    public:
        virtual void Close(void) noexcept = 0;
        virtual ~IRS232(void) noexcept derived_invokes(Close) {}
    };

Focusing on this one line:

        virtual ~IRS232(void) noexcept derived_invokes(Close) {}

Its effect is as follows:

    "Any class derived from IRS232 must contain in its destructor, at
least one invocation of the 'Close' method (even if it's inside the
body of an 'if' statement)."

Similarly if you wanted to make sure that the "Eat_Dinner" method
invokes the "Wash_The_Dishes_Afterward" method, you would do:

        virtual void Eat_Dinner(void) derived_invokes(Wash_The_Dishes);

So then when the compiler goes to compile the translation unit which
contains the definitions of Lazy_Person's methods, it encounters:

    void Lazy_Person::Eat_Dinner(void)
    {
        return; // doesn't call "Wash_The_Dishes"
    }

You get the following compiler error:

main.cpp:11:18: error: method 'Eat_Dinner' in derived class ‘Lazy_Person’ does
                                    not invoke ‘Wash_The_Dishes’, but
method in base
                                    class ‘Person’ is marked
‘derived_invokes(Wash_The_Dishes)’

Of course though this would beg the question . . . what if there's two
kinds of Boost port, such as:

    class RS232_Boost_Win32 : public RS232_Boost { . . . };
    class RS232_Boost_FreeBSD : public RS232_Boost { . . . };

So now we don't want the destructor of "RS232_Boost" to invoke
"Close", but we do want the destructor of "RS232_Boost_Win32" to
invoke "Close", so how do we say this to the compiler? Well how about
the following:

    RS232_Boost::~RS232_Boost(void)
    {
        derived_invokes(Close);
        // Tells the compiler to pass the obligation
        // down to the next derived class
    }

    RS232_Boost_Win32::~RS232_Boost_Win32(void)
    {
        this->Close();
    }

And you can pass the obligation down as far as you want, for example:

    RS232_Boost::~RS232_Boost(void)
    {
        derived_invokes(Close);
    }

    RS232_Boost_Win32::~RS232_Boost_Win32(void)
    {
        derived_invokes(Close);
    }

    RS232_Boost_Win32_x86::~RS232_Boost_Win32_x86(void)
    {
        derived_invokes(Close);
    }

    RS232_Boost_Win32_x86_64::~RS232_Boost_Win32_x86_64(void)
    {
        this->Close();
    }

The purpose of this proposed new feature is that instead of creating
bugs that are discovered a runtime, you get a compiler error.

Received on 2024-05-29 10:38:31