8

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").

ImaginaryHuman072889
  • 4,953
  • 7
  • 19
  • 51
  • Would you need the error to be caught when the code compiles or at run time? – Mike Sep 17 '19 at 22:06
  • @Mike Well, I guess, is there a way to catch it "before runtime"? – ImaginaryHuman072889 Sep 17 '19 at 22:07
  • 1
    Maybe [this](https://stackoverflow.com/questions/274626/what-is-object-slicing) will help? – lakeweb Sep 17 '19 at 22:08
  • I'm sure there is but I'm not sure off the top of my head. My first thought would be to override the `=` operator in your class and find a way for your compiler to catch but I can't say I'm sure of how that would look. https://en.cppreference.com/w/cpp/language/operators – Mike Sep 17 '19 at 22:08
  • 1
    As you can't override the pointer assignment operator, I'm thinking you could make `SpecialString` disallow public access to its `new` operator and require consumers to use a factory-function which returns a custom smart-pointer type that won't expose the raw pointer value. – Dai Sep 17 '19 at 22:09
  • @Mike The `=` operator (I think) does not apply to pointers of the object though? – ImaginaryHuman072889 Sep 17 '19 at 22:09
  • 3
    @Mike You cannot override `=` for pointer assignments because pointers are scalar. – Dai Sep 17 '19 at 22:09
  • Part of the contract you effectively signed when extending `std::string` is `SpecialString` [isa](https://en.wikipedia.org/wiki/Is-a) string and that `ps = pss;` is perfectly legal. Someone might have some voodoo or template wizardry that restricts this, but the result will probably sew more confusion than "just don't do that." – user4581301 Sep 17 '19 at 22:34
  • @user4581301 Fair enough. Just wanted to post the question because Meyers so abruptly ended the topic on it. This was especially unsatisfying because Meyers usually follows up these types of problems with "_... but fortunately there is a way around this_". Broke my heart when he didn't say that this time. 3 – ImaginaryHuman072889 Sep 17 '19 at 22:41
  • IMHO this advice is out of date, covered by the C++17 advice "just don't use the `new` or `delete` keywords". – aschepler Sep 17 '19 at 23:32
  • @aschepler Using the `delete` keyword (and use of the assignment operator for that matter) is just an example of how to get the undefined behavior. Have you thought about something like `std::unique_ptr p{ std::make_unique{} }`? – JaMiT Sep 18 '19 at 00:26
  • @JaMiT Yes, but maybe `unique_ptr>` should disable that conversion... – aschepler Sep 18 '19 at 00:30

3 Answers3

2

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.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • Interesting point about the `protected` destructor - I didn't think of this. But yea, reading the rest of the chapter, Meyers indicates that some classes just straight up were not designed to be used as base classes, so you shouldn't attempt to do this. He also commented that it is unfortunate that C++ does not have any mechanism to prevent inheritance for these types of classes (e.g. similar to `sealed` classes in C#). Although I guess this book is a bit dated, as `final` is now a keyword that does this. Thanks for your help. – ImaginaryHuman072889 Sep 18 '19 at 12:08
1

There's two approaches that might make sense:

  1. 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)

  2. 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.

Michael Anderson
  • 70,661
  • 7
  • 134
  • 187
0

You don't have to check this in runtime if you prevent the error.

As your book says it is a good practice to use a virtual destructor in the base class (with implementation), and of course a destructor for the derived class, this way if you assign a pointer to the derived class - to the base class, it first gets destroyed as a base, and then as a derived object.

you can see an example here https://www.geeksforgeeks.org/virtual-destructor

see comment for more specific clarification

prophet-five
  • 509
  • 3
  • 14
  • The question is, how to prevent this behavior in the code of the _derived class_. Of course, this problem goes away if you have access to the base class - just make the base class destructor virtual, but it is not always the case that you will have the ability to modify the base class (e.g. if your base class is `std::string` as in my question). – ImaginaryHuman072889 Sep 17 '19 at 23:10
  • ok, didn't take in account you meant deriving from classes for which you don't have access... now forbidding the assignment in compile time kind of defeats the concept of polymorphism and overloading it, may force you to slice the object which is somrthing you most-likely won't want to do. therefore i guess there's not much to do but be careful, or implement you own base class – prophet-five Sep 17 '19 at 23:20