I am reading Effective C++ Third Edition by Scott Meyers.
He says generally it is not a good idea to inherit from classes that do not contain virtual functions because of the possibility of undefined behavior if you somehow convert a pointer of a derived class into the pointer of a base class and then delete
it.
This is the (contrived) example he gives:
class SpecialString: public std::string{
// ...
}
SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
ps = pss;
delete ps; // undefined! SpecialString destructor won't be called
I understand why this results in error, but is there nothing that can be done inside the SpecialString
class to prevent something like ps = pss
from happening?
Meyers points out (in a different part of the book) that a common technique to explicitly prevent some behavior from being allowed in a class is to declare a specific function but intentionally don't define it. The example he gave was copy-construction. E.g. for classes that you don't want to allow copy-construction to be allowed, declare a private copy constructor but do not define it, thus any attempts to use it will result in compile time error.
I realize ps = pss
in this example is not copy construction, just wondering if anything can be done here to explicitly prevent this from happening (other than the answer of "just don't do that").
The language allows implicit pointer conversions from a pointer to a derived class to a pointer to its base class, as long as the base class is accessible and not ambiguous. This is not something that can be overridden by user code. Furthermore, if the base class allows destruction, then once you've converted a pointer-to-derived to a pointer-to-base, you can delete the base class via the pointer, leading to the undefined behavior. This cannot be overridden by a derived class.
Hence you should not derive from classes that were not designed to be base classes. The lack of workarounds in your book is indicative of the lack of workarounds.
There are two points in the above that might be worth taking a second look at. First: "as long as the base class is accessible and not ambiguous". (I'd rather not get into the "ambiguous" point.) You can prevent casting a pointer-to-derived to a pointer-to-base in code outside your class implementation by making the base class private
. If you do that, though, you should take some time to think about why you are inheriting in the first place. Private inheritance is typically rare. Often it would make more sense (or at least as much sense) to not derive from the other class and instead have a data member whose type is the other class.
Second: "if the base class allows destruction". This does not apply in your example where you cannot change the base class definition, but it does apply to the claim "generally it is not a good idea to inherit from classes that do not contain virtual [destructors]". There is another viable option. It may be reasonable to inherit from a class that has no virtual functions if the destructor of that class is protected
. If the destructor of a class is protected, then you are not allowed to use delete
on a pointer to that class (outside the implementations of the class and classes derived from it). So you avoid the undefined behavior as long as the base class has either a virtual destructor or a protected one.
There's two approaches that might make sense:
If the real problem is that string
is not really meant to be derived from and you have control over it - then you could make it final
. (Obviously not something you can do with your std::string
though, since you dont control std::string
)
If string
is OK to derive from, but not to use polymorphically, you can remove the new
and delete
functions from SpecialString
to prevent allocating one via new.
For example:
#include <string>
class SpecialString : std::string {
void* operator new(size_t size)=delete;
};
int main() {
SpecialString ok;
SpecialString* not_ok = new SpecialString();
}
fails to compile with:
code.cpp:9:27: error: call to deleted function 'operator new'
SpecialString* not_ok = new SpecialString();
^
code.cpp:4:9: note: candidate function has been explicitly deleted
void* operator new(size_t size)=delete;
Note this doesn't stop odd behaviour like:
SpecialString ok;
std::string * ok_p = &ok;
ok_p->something();
which will always call std::string::something
, not SpecialString::something
if you've provided one. Which may not be what you expect.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With