1

So the basic rule that I find everywhere is that to inherit from a base class, the base class must have a virtual destructor so that the following works:

Base *base = new Inherited();
delete base;

However I am certain I have seen at least one other possibility that allows safe inheritance. However I can't find it anywhere and I feel like I am going mad trying to find it. I thought the other option might have been that the base class had a trivial destructor, but according to Non-virtual trivial destructor + Inheritance, this isn't the case. Even though there wouldn't be a memory leak for this case, it appears this is still undefined behaviour.

Does anyone else know what the other case is or can you definitively tell me that I dreamt it?

Community
  • 1
  • 1
Phil Rosenberg
  • 1,597
  • 1
  • 14
  • 22

6 Answers6

3

I guess an example can be the one that involves shared_ptrs, for it is good to show both the sides of the issue.

Suppose you have a class B with a trivial non virtual destructor and a derived class D with its own complex one.

Let's define the following function somewhere:

shared_ptr<B> factory () {
    // some complex rules at the very end of which you decide to instantiate class D
    return make_shared<D>();
}

In that case you are dealing with all the interesting features due to the polymorphism, but the pointer you are working with has inherited the deleter from the one constructed with type D.

Even though, thanks to the type erasure, the type is buried somewhere and everything works fine, the actual invoked destructor is the one of D, so everything should work fine also from that point of view, even though the destructor of B was not virtual.

Instead, if you define the above factory as:

B* factory () {
    return new D{};
}

The called destructor (well, supposing that someone will delete it) will be the one of B, that is not what you want.

That said, defining as virtual the destructor of a class that is meant to be inherited from is a good practice, otherwise put a final in the class definition and stop there the hierarchy.

There also a lot of other examples, this is not the only case where it works, but it can help to explain why it works.

skypjack
  • 49,335
  • 19
  • 95
  • 187
2

Perhaps when the inheritance is private. In such a case, the user can't convert Derived* to Base* so there is no chance of trying to delete the derived class through the base class pointer. Of course, you still have to watch that you don't do this anywhere within your implementation of Derived.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • There's a loophole. It's very unlikely to happen in real code, but still: a C-style cast *can* legally achieve the conversion to `Base*` and then invoke undefined behaviour via the `delete`. Shortest example I could come up with: `class Base {}; class Derived : Base {}; int main() { Base* b = (Base*)(new Derived); delete b; }` See http://stackoverflow.com/questions/33899978/a-conversion-that-c-style-cast-can-handle-but-c-casts-cannot/33900968#33900968 for the special C-style cast rule. – Christian Hackl Nov 29 '15 at 11:33
2

My take on this is pragmatic rather than anything to do with what is or isn't allowed by the standards.

So, pragmatically, if a class doesn't have a virtual destructor - even an empty one - then my default assumption is that it hasn't been designed to be used as a base class. This may have more implications than just destruction and in more cases than not, just opens a can of worms for you to fall in later.

If you want or need to use functionality from a class without a virtual destructor, it would be safer to use composition rather than inheritance. In fact, that's the preferred route anyway.

Roger Rowland
  • 25,885
  • 11
  • 72
  • 113
  • You should mention that your take on this is violated by some C++ standard classes, e.g. `std::iterator` or `std::stack`. Your guideline is good enough in most situations but is not as universal as you seem to imply. – Christian Hackl Nov 29 '15 at 11:39
  • @ChristianHackl Well, you've just mentioned that ;-) As I said above, this is pragmatism and is based on experience. I acknowledge that I err on the side of caution, which has been a useful tenet over 38 years of software development! – Roger Rowland Nov 29 '15 at 11:58
  • @RogerRowlan Actually the answer by Itjax gives another safe case, which is the case I was looking for. – Phil Rosenberg Nov 30 '15 at 10:14
  • @ChristianHackl, I'd be interested to know how std::iterator and std::stack deviate and how they avoid undefined behaviour - maybe just by being careful about destruction? Feel free to post an answer. – Phil Rosenberg Nov 30 '15 at 10:15
  • @PhilRosenberg: `std::iterator` and `std::stack` do not have a virtual destructor but are designed to be (possible) base classes. See http://en.cppreference.com/w/cpp/iterator/iterator: *"(...) the base class provided to simplify definitions of the required types for iterators."* As for `std::stack`, it has a protected `c` member, allowing subclasses access to the wrapped container. Not terribly common use cases, but still, those are standard classes. – Christian Hackl Nov 30 '15 at 14:42
  • @PhilRosenberg: Re "how they avoid undefined behaviour". They don't. **You** have to avoid it by not attempting a polymorphic `delete`. – Christian Hackl Nov 30 '15 at 14:43
1

The other case I've seen mentioned is making the base-class destructor protected. That way, you prevent deletion through a base class.

This is actually item 50 in the book C++ Coding Standards by Herb Sutter et al: "Make base class destructors public and virtual or protected and non-virtual", so it is quite likely that you have heard of it before.

ltjax
  • 15,837
  • 3
  • 39
  • 62
  • 1
    This is the other safe option I was looking for, well done for noting I said "safely". When I first found it I didn't understand it, which is why I didn't remember it. But as you say it means only derived classes can delete the base class avoiding the possible problem. It also mean only derived classes or internal functions can construct the base too. – Phil Rosenberg Nov 30 '15 at 10:03
0

You can always inherit from a class. There are rules to obey though, e.g. without a virtual destructor you can't invoke the destructor polymorphically. In order to avoid this, you could e.g. use private derivation for baseclasses that were not intended as baseclasses, like e.g. the containers from the STL.

Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
  • I agree, you can always inherit; but, it is your responsibility is for the resulting class and making sure it is safe. The need of a polymorphic destructor is only necessarily if the derived class does something critical in its destructor (like freeing a dynamic resource). There are ways around that too such as using containment since the destructor of members will always be called even without a polymorphic destructor. – markshancock Nov 29 '15 at 09:47
  • 2
    "You can always inherit from a class" - unless it's marked `final` – M.M Nov 29 '15 at 09:49
  • Wrong language, @M.M, ;) – Ulrich Eckhardt Nov 29 '15 at 09:50
  • 3
    @UlrichEckhardt I'm talking about C++ (11 and later) – M.M Nov 29 '15 at 09:52
  • I'm not sure whether you're right with that statement, @markshancock, I could also imagine that invoking the wrong destructor causes undefined behaviour. – Ulrich Eckhardt Nov 29 '15 at 09:52
0

As others have mentioned, as long as you delete the class through it's own destructor - in other words you do

Inherited *ip = new Inherited();
Base *p = ip; 
... 
delete ip;

you'll be fine. There are several different ways to do that, but you have to be quite careful to ensure that is the case.

However, having an empty destructor in the baseclass [and your inherited type is immediately inheriting] only works as long as it is TRULY empty, and not just that you have an { } for the body of the destructor. [See Edit2 below!]

For example, if you have a vector or std::string, or whatever other class that needs destruction, in your baseclass, then you will leak the content of that class. In other words, you need to make 100% sure that the destructor of the baseclass is empty. I don't know of a programmatic way to determine that (beyond analysing the generated code, that is).

Edit:

Also beware of "changes in the future" - for example, adding a string or vector inside Base or changing the base class from Base to SomethingInheritedFromBase that has a destructor "with content" will ruin the "empty destructor" concept.

Edit2:

It should be noted that for the "destructor is empty", you have to have true empty destuctors in all derived clases too. There are classes that have no members that need destruction (interface classes typically have no data members, for example, so would not need destruction in themselves), so you could construct such a case, but again, we have to be VERY careful to avoid the destructor of a derived class adding a destructor into the class.

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
  • I don't understand your last two paragraphs. They seem to imply that an empty (`{}`) destructor will somehow prevent destructors of member variables from running, or that you can somehow prevent the derived destructor from calling the base destructor, but both statements are not correct. – Christian Hackl Nov 29 '15 at 11:49
  • @ChristianHackl: I have clarified in such a way that I think it's more clear and correct. – Mats Petersson Nov 29 '15 at 12:04
  • Your "truly empty" destructor is the Trivial destructor that is already defined elsewhere and mentioned in my post. Actually it doesn't help at all. It neither prevents the direct calling of a non-virtual base destructor, nor does it avoid undefined behaviour. It does avoid a memory leak, but if we have undefined behaviour then that is the least of our worries. – Phil Rosenberg Nov 30 '15 at 10:08