Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 404
ImaginaryHuman072889 Avatar asked Sep 17 '19 22:09

ImaginaryHuman072889


2 Answers

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.

like image 184
JaMiT Avatar answered Nov 04 '22 04:11

JaMiT


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.

like image 1
Michael Anderson Avatar answered Nov 04 '22 02:11

Michael Anderson