C++ Logo

std-proposals

Advanced search

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

From: Sebastian Wittmeier <wittmeier_at_[hidden]>
Date: Wed, 29 May 2024 15:27:11 +0200
For convenience the RS232Handle can act like a smart pointer with an operator-> to call member functions of RS232.   -----Ursprüngliche Nachricht----- Von:Richard Hodges via Std-Proposals <std-proposals_at_[hidden]> Gesendet:Mi 29.05.2024 13:29 Betreff:Re: [std-proposals] Call virtual method from base‘s destructor An:std-proposals_at_[hidden]; CC:Richard Hodges <hodges.r_at_[hidden]>; You are conflating object lifetime with logical lifetime. A more correct design would be to have the IRS232 owned by (say) a unique_ptr who's embedded destructor calls the Close method on the owned IRS232 before then deleting it.. for example: #include <memory> #include <iostream> #include <string>  class IRS232  { public:     virtual void Close(void) noexcept = 0;     virtual ~IRS232(void) noexcept = default; }; struct IRS232Deleter {     void operator()(IRS232* p) const noexcept     {         if (p)         {             p->Close();             delete p;         }     } }; struct RS232Handle {     template<class Derived, class...Args>     static RS232Handle construct(Args&&...args)     {         return RS232Handle(impl_type(new Derived (std::forward<Args>(args)...), IRS232Deleter()));     } private:     using impl_type = std::unique_ptr<IRS232, IRS232Deleter>;     RS232Handle(impl_type impl)     : impl_(std::move(impl))     {     }     impl_type impl_; }; // to use struct TestRS232 : IRS232 {     TestRS232(std::string name)     : name_(std::move(name))     {         std::cout << "TestRS232 " << name_ << " constructed\n";     }     ~TestRS232() noexcept override     {         std::cout << "TestRS232 " << name_ << " destroyed\n";     }     void Close() noexcept override     {         std::cout << "TestRS232 " << name_ << " closing\n";     }     std::string name_; }; int main() {     auto rs232 = RS232Handle::construct<TestRS232>("bob"); } Expected output: TestRS232 bob constructed         TestRS232 bob closing TestRS232 bob destroyed On Wed, 29 May 2024 at 12:38, Frederick Virchanza Gotham via Std-Proposals <std-proposals_at_[hidden] <mailto:std-proposals_at_[hidden]> > wrote: 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. -- Std-Proposals mailing list Std-Proposals_at_[hidden] <mailto:Std-Proposals_at_[hidden]> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals -- Std-Proposals mailing list Std-Proposals_at_[hidden] https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

Received on 2024-05-29 13:27:15