Date: Wed, 29 May 2024 13:29:33 +0200
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]> 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]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
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]> 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]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
Received on 2024-05-29 11:29:46