Date: Wed, 29 May 2024 11:49:18 +0000
This is a violation of many design principles. Chief among them the symmetry of resource management.
But regardless there are many reasons as to why there are special rules to calling virtual methods from constructors and destructors.
1. The base class doesn't know what derived is in order to call it (much less possible version of it.
2. Even if you could solve that (which you won't), the life-time of the derived object is already expired or hasn't even started, there's no state to make a coherent use of it.
The class responsible for managing the resource should... manage the resource.
Your complaint that "you need to call close on every derived that adds a resource", is not convincing, as this is a O(n) problem and the cost to a developer to write it down (single call to close in a destructor, max 4 lines of code, 2 of those are brackets) is going to pale in comparison to creating the resource and using it in the first place.
________________________________
From: Std-Proposals <std-proposals-bounces_at_[hidden]> on behalf of Frederick Virchanza Gotham via Std-Proposals <std-proposals_at_[hidden]>
Sent: Wednesday, May 29, 2024 12:38:14 PM
To: std-proposals <std-proposals_at_[hidden]>
Cc: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Subject: [std-proposals] Call virtual method from base's destructor
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.
But regardless there are many reasons as to why there are special rules to calling virtual methods from constructors and destructors.
1. The base class doesn't know what derived is in order to call it (much less possible version of it.
2. Even if you could solve that (which you won't), the life-time of the derived object is already expired or hasn't even started, there's no state to make a coherent use of it.
The class responsible for managing the resource should... manage the resource.
Your complaint that "you need to call close on every derived that adds a resource", is not convincing, as this is a O(n) problem and the cost to a developer to write it down (single call to close in a destructor, max 4 lines of code, 2 of those are brackets) is going to pale in comparison to creating the resource and using it in the first place.
________________________________
From: Std-Proposals <std-proposals-bounces_at_[hidden]> on behalf of Frederick Virchanza Gotham via Std-Proposals <std-proposals_at_[hidden]>
Sent: Wednesday, May 29, 2024 12:38:14 PM
To: std-proposals <std-proposals_at_[hidden]>
Cc: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Subject: [std-proposals] Call virtual method from base's destructor
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:49:22