31

In these slides about C++11/14 standard, on slide 15, the author writes that "many classic coding rules [are] no longer applicable" in C++11. He proposes a list of three examples, and I agree with the Rule of Three and the memory management.

However his second example is "Virtual destructor with virtual members" (just that). What does it mean? I know one must declare as virtual the base class destructor in order to call the right destructor if we have something like

Base *b = new Derived;
...
delete b;

This is well explained here: When to use virtual destructors?

But is it useless now in C++11 to declare virtual your destructor if you have virtual members?

Community
  • 1
  • 1
Florian Richoux
  • 1,543
  • 1
  • 14
  • 28
  • 8
    This is just a guess, but when using smart pointers, you can arrange things such that the right destructor gets called, even if the base destructor is not virtual. This happens out of the box with `shared_ptr`. – juanchopanza Apr 02 '14 at 09:08
  • 2
    The statements to the effect that the rule of three or the need for a virtual destructor are no longer necessary are simply false. None of the new features have changed anything in this regard. – James Kanze Apr 02 '14 at 11:21
  • @JamesKanze About the Rule of Three, the author could also mean that it is deprecated, since now it is the Rule of Four/Five. For the Rule of Zero, I truly believe it is useful, but under the condition that your classes use the RAII principle for all resources they own. – Florian Richoux Apr 02 '14 at 11:46
  • @FlorianRichoux Not really. Most classes won't need the extra complexity of supporting move. (And making a class which uses resources which must be freed work correctly usually need a lot more than what the usual RAII classes can do.) – James Kanze Apr 02 '14 at 11:50
  • 1
    @JamesKanze, you're missing the point. If all your non-trivial members have a correct destructor or are an RAII type with a suitable deleter (whether that just deletes memory or performs more complicated cleanup) then _there is no extra complexity supporting move_. You just say `Foo(Foo&&) = default;` and it Just Works™. And because all your members clean themselves up, you can also default the destructor. It requires a different approach to class design but that's the approach Prof. Sommerlad is advocating in those slides. (Not sure about the virtual bit though, I'll ask him.) – Jonathan Wakely Apr 04 '14 at 09:48
  • @FlorianRichoux: the answer by PeterSom seems perfect to me... – Massa Apr 09 '14 at 23:19
  • @Messa It is indeed! I was asking for details about these slides, and get an answer from the author himself. :) I will reward his answer with both the bounty and the best answer as soon as the bounty finish (tomorrow). I doubt there will be better answer... – Florian Richoux Apr 09 '14 at 23:55

4 Answers4

37

As the author of the slides I'll try to clarify.

If you write code explicitly allocating a Derived instance with new and destroying it with delete using a base class pointer then you need to define a virtual destructor, otherwise you end up with incompletely destroying the Derived instance. However, I recommend to abstain from new and delete completely and use exclusively shared_ptr for referring to heap-allocated polymorphic objects, like

shared_ptr<Base> pb=make_shared<Derived>();

This way, the shared pointer keeps track of the original destructor to be used, even if shared_ptr<Base> is used to represent it. Once, the last referring shared_ptr goes out of scope or is reset, ~Derived() will be called and the memory released. Therefore, you don't need to make ~Base() virtual.

unique_ptr<Base> and make_unique<Derived> do not provide this feature, because they don't provide the mechanics of shared_ptr with respect to the deleter, because unique pointer is much simpler and aims for the lowest overhead and thus is not storing the extra function pointer needed for the deleter. With unique_ptr the deleter function is part of the type and thus a uniqe_ptr with a deleter referring to ~Derived would not be compatible with a unique_ptr<Base> using the default deleter, which would be wrong for a derived instance anyway, if ~Base wasn't virtual.

The individual suggestions I make, are meant to be easy to follow and followed all together. They try to produce simpler code, by letting all resource management be done by library components and the compiler generated code.

Defining a (virtual) destructor in a class, will prohibit a compiler-provided move constructor/assignment operator and might prohibit also a compiler provided copy constructor/assignment operator in future versions of C++. Resurrecting them has become easy with =default, but still looks like a lot of boilerplate code. And the best code is the code you don't have to write, because it can not be wrong (I know there are still exceptions to that rule).

To summarize "Don't define a (virtual) destructor" as a corollary to my "Rule of Zero":

Whenever you design a polymorphic (OO) class hierarchy in modern C++ and want/need to allocate its instances on the heap and access them through a base class pointer use make_shared<Derived>() to instantiate them and shared_ptr<Base> to keep them around. This allows you to keep the "Rule of Zero".

This doesn't mean you must allocate all polymorphic objects on the heap. For example, defining a function taking a (Base&) as parameter, can be called with a local Derived variable without problems and will behave polymorphic, with respect to virtual member functions of Base.

In my opinion dynamic OO polymorphism is heavily overused in many systems. We shouldn't program like Java, when we use C++, unless we have a problem, where dynamic polymorphism with heap allocated objects is the right solution.

PeterSom
  • 2,067
  • 18
  • 16
  • I frequently refactor my inheritance-structures and sometimes end up with some other class as ultimate base class, how will this be handled in the case of shared_ptr pb=make_shared(); as the model used ? – Jojje Apr 10 '14 at 11:28
  • I am not sure, I understand your concern right. If `Base` is a base class of `Derived` my arguments are still valid. However, if `Base` is completely unrelated to `Derived` then this shouldn't compile. – PeterSom Apr 10 '14 at 22:09
  • 2
    I think that NOT defining virtual destructor for a class which is intended to be used polymorphically imposes a big burden on the users of the class - they are strictly required to hold them with shared_ptr. But shared_ptr is very much discouraged and considered overused and should be replaced by unique_ptr whenever possible. So I believe not defining virtual destructor causes much worse issues than accepting the fact that you have to mark copy and move constructor and assignment operator as =default. I think C++11 changed nothing about when and how to use virtual destructors. – HiFile.app - best file manager Jun 24 '16 at 12:50
  • 2
    This doesn't seem like very good advice - you're saving a trivial amount of (mental) overhead at the class declaration, in exchange for imposing a non-trivial (mental) overhead by restricting client usage in a rather unexpected way. You're also trading a small overhead of a virtual lookup once when an object is destroyed vs... a small virtual lookup once an object is destroyed. That doesn't seem all that helpful to me. – Cubic Nov 29 '16 at 22:06
  • The advice is applicable only under specific conditions as described by the author. However, the statement in the slide gives the impression that somehow C++11 has changed the behavior with respect to virtual destructor - which is not the case. This "generalized" statement is quite misleading. – ap-osd Feb 10 '20 at 14:34
2

I think that this is to do with the "rule of zero" mentioned elsewhere in the presentation.

If you only have automatic member variables (i.e. use shared_ptr or unique_ptr for members that would otherwise be raw pointers) then you don't need to write your own copy or move constructors, or assignment operators -- the compiler-provided defaults will be optimal. With in-class initialisation, you don't need a default constructor either. And finally, you don't need to write a destructor at all, virtual or not.

Tristan Brindle
  • 16,281
  • 4
  • 39
  • 82
  • Yes, but according to Scott Meyers, it remains preferable to explicitly declare the copy/move ctors, the copy/move assignment operators and the destructor as `default` (http://scottmeyers.blogspot.fr/2014/03/a-concern-about-rule-of-zero.html). Thus following this amended Rule of Zero, I guess one still need to declare the base destructor to be virtual. – Florian Richoux Apr 02 '14 at 10:11
  • It's kind of silly that if there is a virtual member somewhere, then it is UB to not have a virtual destructor; but if there is no virtual member than it is wasteful to have a virtual destructor. That's fragile; is there any reason why destuctors shouldn't "automatically" be virtual in a class that's already got a vtable, and non-virtual in others? – M.M Apr 04 '14 at 11:49
  • 1
    I believe Scott Meyers is too much sticking to his own past, when he discusses the "Rule of Zero". I am trying to keep things as simple as possible. Defining the usually compiler-provided special member functions (correctly!) should be a feature left for library experts and not something happening in regular code most C++ programmers create. – PeterSom Apr 04 '14 at 12:07
  • 1
    @Matt McNabb: if you follow my rules then you won't get UB without a virtual destructor and you would never come to the situation to write a virtual destructor to produce unnecessary overhead. – PeterSom Apr 04 '14 at 12:08
  • 2
    "Your rules" being to only ever use `shared_ptr` to point to polymorphic objects? OK, although I am still happier if a class definition is correct in itself, without relying on the user to use a particular idiom. Those users can do strange things sometimes... – M.M Apr 04 '14 at 12:20
  • +1 mcnabb for mentioning the difference between defining the class and using the class. It would be poor form to define a class that had to be used with a certain ptr type. – Mike Apr 09 '14 at 20:54
0

The paper linked shows the relevant code:

std::unique_ptr<Derived> { new Derived };

The stored deleter is std::default_delete<Derived>, which doesn't require Base::~Base to be virtual.

Now you can move this to a unique_ptr<Base>, and it will also move the std::default_delete<Derived> without converting it to a std::default_delete<Base>.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • 1
    I see, it makes sense indeed. Thanks to you and juanchopanza! – Florian Richoux Apr 02 '14 at 09:26
  • Actually, that's only true for `std::shared_ptr`, not `std::unique_ptr`. – milleniumbug Apr 02 '14 at 09:45
  • @milleniumbug: Corrected to match paper. – MSalters Apr 02 '14 at 09:50
  • 1
    I would still make the destructor virtual. It does not hurt and if someone uses your class in the old way it will still work. – Danvil Apr 02 '14 at 09:53
  • 4
    This do not works, only base destructor will be called : [show here](http://coliru.stacked-crooked.com/a/9ca3b178c86d58e3). moving do not change the type of the receiver and the deleter is part of it. It would require type erasure like shared_ptr other else. – galop1n Apr 02 '14 at 09:53
  • 1
    @galop1n: Good point, I was trying to reverse-engineer what the paper was arguing and it seems far too fragile. I don't think you need to full type erasure of `shared_ptr` for the simple OO case, but what `unique_ptr` offers is indeed insufficient. – MSalters Apr 02 '14 at 10:08
  • 1
    @Danvil I would use virtual dtors too, but it **can** hurt. It could make a type polymorphic if it wasn't yet, introducing overhead and potentially chaning runtime semantics (`typeid` and `dynamic_cast`) – sehe Apr 02 '14 at 10:10
  • @lukasz1985: Mostly, that's why we C++ programmers usually skip the whole pointer part (smart or not) entirely. But there's `make_shared` to keep the overhead down. – MSalters Apr 02 '14 at 10:48
  • There's actually a problem here, since some coding guidelines will insist that the detructors of the derived classes be declared private (so that you don't accidentally create an instance on the stack). – James Kanze Apr 02 '14 at 11:24
0

To answer the specific question...

But is it useless now in C++11 to declare virtual your destructor if you have virtual members?

The need for a virtual destructor has NOT changed in the C++11 core language. You must declare your destructor as virtual if you are deleting the derived object using the base pointer.

The statement in the slide gives the impression that somehow C++11 has changed the behavior with respect to virtual destructor - which is not the case. As the author has clarified, it's only applicable when using a shared_ptr. But the fact that a virtual destructor is still required (except with the use shared_ptr) gets diluted in the long explanation.

ap-osd
  • 2,624
  • 16
  • 16