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/
https://blog.knatten.org) and ‘C++ Quiz’ (https://cppquiz.org/).
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’ (