ImaginaryHuman072889
ImaginaryHuman072889

Reputation: 5185

Prevent pointer reassignment

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

Upvotes: 8

Views: 664

Answers (3)

JaMiT
JaMiT

Reputation: 16853

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.

Upvotes: 2

Michael Anderson
Michael Anderson

Reputation: 73490

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.

Upvotes: 1

prophet-five
prophet-five

Reputation: 559

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

Upvotes: 0

Related Questions