No Move vs Deleted Move Constructors

No Move vs Deleted Move Constructors

By Anders Knatten

Overload, 29(166):22-23, December 2021


C++ allows you to mark constructors as deleted. Anders Knatten reveals what a deleted definition means in practice.

It’s easy to think that deleting the move constructor means removing it. So if you do MyClass(MyClass&&) = delete, you make sure it doesn’t get a move constructor. This is, however, not technically correct. It might seem like a nitpick, but it actually gives you a less useful mental model of what’s going on.

First: When does this matter? It matters for understanding in which cases you’re allowed to make a copy/move from an rvalue.

Listing 1 contains some examples of having to copy/move an object of type MyClass.

MyClass obj2(obj1);
MyClass obj3(std::move(obj1));

MyClass obj4 = obj1;
MyClass obj5 = std::move(obj1);

return obj1;
return std::move(obj1);
			
Listing 1

They are all examples of ‘direct initialization’ (the first two) and ‘copy initialization’ (the last four). Note that there is no concept of ‘move initialization’ in C++. Whether you end up using the copy or the move constructor to initialize the new object is just a detail.

For the rest of this article, let’s just look at copy initialization; direct initialization works the same way for our purposes. In any case, you create a new copy of the object, and the implementation uses either the copy or the move constructor to do so.

Let’s first look at a class NoMove (Listing 2).

struct NoMove
{
  NoMove();
  NoMove(const NoMove&);
};
			
Listing 2

This class has a user-declared copy constructor, so it doesn’t automatically get a move constructor :

If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if [C++Standard_1]:

  • X does not have a user-declared copy constructor
  • (…)

So this class doesn’t have a move constructor at all. You didn’t explicitly declare one, and none got implicitly declared for you.

On the other hand, let’s see what happens if we explicitly delete the move constructor (Listing 3).

struct DeletedMove
{
  DeletedMove();
  DeletedMove(const DeletedMove&);
  DeletedMove(DeletedMove&&) = delete;
};
			
Listing 3

This is called ‘a deleted definition’:

A function definition of the form:

    (…) = delete ;

is called a deleted definition. A function with a deleted definition is also called a deleted function. [C++Standard_2]

Importantly, that does not mean that its definition has been deleted/removed and is no longer there. It means that is has a definition, and that this particular kind of definition is called a ‘deleted definition’. I like to read it as ‘deleted-definition’.

So our NoMove class has no move constructor at all. Our DeletedMove class has a move constructor with a deleted definition.

Why does this matter?

Let’s first look at a class with both a copy and a move constructor, and how to copy-initialize it (Listing 4).

struct Movable
{
  Movable();
  Movable(const Movable&);
  Movable(Movable&&);
};
Movable movable;
Movable movable2 = movable;
			
Listing 4

When initializing movable2, we need to find a function to do that with. A copy constructor would do nicely. And since we do have a copy constructor, it indeed gets used for this.

What if we turn movable into an rvalue?

  Movable movable2 = std::move(movable);

Now a move constructor would be great. And we do have one, and it indeed gets used.

But what if we didn’t have a move constructor? That’s the case with our class NoMove in Listing 2.

This one has a copy constructor, so it doesn’t get a move constructor. We can, of course, still make copies using the copy constructor:

  NoMove noMove;
  NoMove noMove2 = noMove;

But what happens now?

  NoMove noMove;
  NoMove noMove2 = std::move(noMove);

Are we now ‘move initializing’ noMove2 and need the move constructor? Actually, we’re not. We’re still copy-initializing it, and need some function to do that task for us. A move constructor would be great, but a copy constructor would also do. It may be less efficient, but of course you’re allowed to make a copy of an rvalue.

So this is fine, the code compiles, and the copy constructor is used to make a copy of the rvalue.

What happened behind the scenes in all the examples above, is overload resolution. Overload resolution looks at all the candidates to do the job, and picks the best one. In the cases where we initialize from an lvalue, the only candidate is the copy constructor. We’re not allowed to move from an lvalue. In the cases where we initialize from an rvalue, both the copy and the move constructors are candidates. But the move constructor is a better match, as we don’t have to convert the rvalue to an lvalue reference. For Movable, the move constructor got selected. For NoMove, there is no move constructor, so the only candidate is the copy constructor, which gets selected.

Now, let’s look at what’s different when instead of having no move constructor, we have a move constructor with a deleted definition (Listing 5).

struct DeletedMove
{
    DeletedMove();
    DeletedMove(const DeletedMove&);
    DeletedMove(DeletedMove&&) = delete;
};
			
Listing 5

We can of course still copy this one as well:

  DeletedMove deletedMove2 = deletedMove;

But what happens if we try to copy-initialize from an rvalue?

  DeletedMove deletedMove2 =
    std::move(deletedMove);

Remember, overload resolution tries to find all candidates to do the copy-initialization. And this class does in fact have both a copy and a move constructor, which are both candidates. The move constructor is picked as the best match, since again we avoid the conversion from an rvalue to an lvalue reference. But the move constructor has a deleted definition, and the program does not compile.

A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed. [Note: This includes calling the function implicitly or explicitly (…) If a function is overloaded, it is referenced only if the function is selected by overload resolution.(…)] [C++Standard_2]

The function is being called implicitly here, we’re not manually calling the move constructor. And we can see that this applies because overload resolution selected to use the move constructor with the deleted definition.

So the differences between not declaring a move constructor and defining one as deleted are:

  • The first one does not have a move constructor, the second one has a move constructor with a deleted definition.
  • The first one can be copy-initialized from an rvalue, the second cannot.

References

[C++Standard_1] C++ standard: ‘Copying and moving class objects’, available from https://timsong-cpp.github.io/cppwp/n4659/class.copy

[C++Standard_2] C++ standard: ‘Delete definitions’, available from https://timsong-cpp.github.io/cppwp/n4659/dcl.fct.def.delete

This article was previously published online at: https://blog.knatten.org/2021/10/

Anders Knatten Anders started programming in Turbo Pascal in 1995, and has been programming professionally in various languages since 2001. He’s currently a senior developer at Zivid, making 3D cameras for robot vision. He’s the author of the blog ‘C++ on a Friday’ (https://blog.knatten.org) and ‘C++ Quiz’ (https://cppquiz.org/).






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.