C++11 Virtual Destructors and Auto Generation of Move Special Functions

C++11 virtual destructors and auto generation of move special functions

  1. No, a defaulted destructor is still considered user defined, so it will prevent the generation of move operations. Also declare the move operations default-ed to make the compiler generate them.

  2. You need to only declare the move operations as default-ed in the base class. In the derived class, the destructor won't be user defined anymore (unless you explicitly say so), so the move operations won't be deleted.

So what I'd do is the following:

class Base
{
virtual ~Base() = default;
Base(Base&&) = default;
Base& operator=(Base&&) = default;
// probably need to think about copy operations also, as the move disables them
Base(const Base&) = default;
Base& operator=(const Base&) = default;
};

I highly recommend this talk by the person who contributed probably the most to the move semantics: http://www.slideshare.net/ripplelabs/howard-hinnant-accu2014

Or, if you can get your hands on, you should read the Item 17: Understand special member function generation from Scott Meyers' excellent book Effective Modern C++. This issue is excellently explained.

PS: I think you should think a bit more about your base classes. Most of the time, you should use abstract classes, so there will be no need to copy/move instances of them.

PSS: I think by default destructors are marked noexcept in C++11/14, so not explicitly specifying it shouldn't cause any problems:

Inheriting constructors and the implicitly-declared default
constructors, copy constructors, move constructors, destructors,
copy-assignment operators, move-assignment operators are all
noexcept(true) by default, unless they are required to call a function
that is noexcept(false), in which case these functions are
noexcept(false).

Does a default virtual destructor prevent compiler-generated move operations?

Yes, declaring any destructor will prevent the implicit-declaration of the move constructor.

N3337 [class.copy]/9: If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared
as defaulted if and only if

  • X does not have a user-declared copy constructor,
  • X does not have a user-declared copy assignment operator,
  • X does not have a user-declared move assignment operator,
  • X does not have a user-declared destructor, and
  • the move constructor would not be implicitly defined as deleted.

Declaring the destructor and defining it as default counts as user-declared.

You'll need to declare the move constructor and define it as default yourself:

WidgetBase(WidgetBase&&) = default;

Note that this will in turn define the copy constructor as delete, so you'll need to default that one too:

WidgetBase(const WidgetBase&) = default;

The rules for copy and move assignment operators are pretty similar as well, so you'll have to default them if you want them.

How to declare the virtual destructor without breaking move and copy constructors

First I would consider whether Foo really needs a virtual destructor. Maybe you can solve your problem in a type safe manner using a simple template, saving you from messing with pointers and casting and so on.

If you decide on making Foo virtual, then I would recommend this abstraction.

class VirtualDestructor
{
protected:
VirtualDestructor() = default;
virtual ~VirtualDestructor() = default;
VirtualDestructor(const VirtualDestructor & /* other */) = default;
VirtualDestructor &operator=(const VirtualDestructor & /* other */) = default;
VirtualDestructor(VirtualDestructor && /* other */) = default;
VirtualDestructor &operator=(VirtualDestructor && /* other */) = default;
};

Put this in a library in an appropriate namespace. Then you can keep Foo and all other virtual classes clean.

class Foo : VirtualDestructor
{
public:
Foo();
};

The same technique can also be used when deleting for example copy constructors.

Edit:
Compiler output and diff with original code

Virtual destructor with virtual members in C++11

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.

Defaulted destructor in base class disable move constructor in child class if there is a member

The implicit move constructor is not (only) deleted, it is not declared in the first place when you have a user-declared destructor, as is the case with Base1 and Base2.

Therefore the move constructor can never be considered in overload resolution and so auto obj1 = std::move(c1);, while it can call Child1's move constructor, needs to fall back to copy construction for the Base1 subobject.

The implicitly-declared copy constructors of both Base1 and Child1 are defined as deleted, because Data's implicitly-declared copy constructor is defined as deleted, because Data has a user-defined move constructor. Therefore auto obj1 = std::move(c1); will fail with an error that the implicitly-declared copy constructor is deleted.

For Base2 the copy constructor is not defined as deleted, because it doesn't have a Data member and so auto obj2 = std::move(c2); will call Child2's move constructor (which also uses Data's move constructor), but use the copy constructor for the Base2 subobject.

Why does destructor disable generation of implicit move methods?

"The Rule of Zero" is in fact about something else than what special member functions are generated and when. It is about a certain attitude to class design. It encourages you to answer a question:

Does my class manage resources?

If so, each resource should be moved to its dedicated class, so that your classes only manage resources (and do nothing else) or only accumulate other classes and/or perform same logical tasks (but do not manage resources).

It is a special case of a more general Single Responsibility Principle.

When you apply it, you will immediately see that for resource-managing classes you will have to define manually move constructor, move assignment and destructor (rarely will you need the copy operations). And for the non-resource classes, you do not need to (and in fact you probably shouldn't) declare any of: move ctor/assignment, copy ctor/assignment, destructor.

Hence the "zero" in the name: when you separate classes to resource-managing and others, in the "others" you need to provide zero special member functions (they will be correctly auto-generated.

There are rules in C++ what definition (of a special member function) inhibits what other definitions, but they only distract you from understanding the core of the Rule of Zero.

For more information, see:

  1. https://akrzemi1.wordpress.com/2015/09/08/special-member-functions/
  2. https://akrzemi1.wordpress.com/2015/09/11/declaring-the-move-constructor/

Need some help understanding C++11 Move Constructors

There are two moves that could happen in your program:

  1. From the function to the return object.
  2. From the return object to model.

Both of these moves can be elided by the compiler for the same reason:

when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

There are other situations in which copy/move elision occurs (see §12.8/31 in C++11). Note that copy/move elision is entirely optional - the compiler doesn't have to do it.

Note that the compiler is allowed to optimize absolutely anything away as long as it doesn't change the behaviour of your program (under the as-if rule). The reason that copy/move elision is explicitly mentioned in the standard is because it might change the behaviour of your program if your copy/move constructors have side effects. The compiler is allowed to perform this optimization even if it changes the behaviour of your program. This is why your copy/move constructors should never have side effects, because then your program will have multiple valid execution paths.

You can pass the -fno-elide-constructors option to gcc to ensure that this optimization is never performed.

What are the rules for automatic generation of move operations?

From the standard Ch. 12 - Special member functions

Par 12.8 Copying and moving class objects (emphasis mine)

9 . If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared as defaulted if and only if

— X does not have a user-declared copy constructor,

— X does not have a user-declared copy assignment operator,

— X does not have a user-declared move assignment operator, and

— X does not have a user-declared destructor.

[ Note: When the move constructor is not implicitly declared or explicitly supplied, expressions that
otherwise would have invoked the move constructor may instead invoke a copy constructor. —end note ]

Then 11 explains the rules for deleting the defaulted move constructor

11 . An implicitly-declared copy/move constructor is an inline public member of its class. A defaulted copy/ move constructor for a class X is defined as deleted (8.4.3) if X has:

— a variant member with a non-trivial corresponding constructor and X is a union-like class,

— a non-static data member of class type M (or array thereof) that cannot be copied/moved because overload resolution (13.3), as applied to M’s corresponding constructor, results in an ambiguity or a function that is deleted or inaccessible from the defaulted constructor,

— a direct or virtual base class B that cannot be copied/moved because overload resolution (13.3), as applied to B’s corresponding constructor, results in an ambiguity or a function that is deleted or inaccessible from the defaulted constructor,

— any direct or virtual base class or non-static data member of a type with a destructor that is deleted or inaccessible from the defaulted constructor, or,

— for the copy constructor, a non-static data member of rvalue reference type. A defaulted move constructor that is defined as deleted is ignored by overload resolution (13.3, 13.4).

[ Note: A deleted move constructor would otherwise interfere with initialization from an rvalue which can use the copy constructor instead. —end note ]



On the complexity of it all *

The rules can be somewhat overwhelming. It's good to use some technique to bypass the complexity. Examples are :

  1. Make use of the rule of zero to simplify the writing of the majority of your classes.
  2. (On implicitly deleted) Explicitly default the special member function in question; if it would have been implicitly defined as deleted, the compiler will complain.

* points made in the comments by myself (1) and dyp (2)



Related Topics



Leave a reply



Submit