9
votes

When a class explicitly declares a copy operation (i.e., a copy constructor or copy assignment operator), move operations are not declared for the class. But when a class explicitly declares a move operation, the copy operations are declared as deleted. Why does this asymmetry exist? Why not just specify that if a move operation is declared, no copy operations will be declared? From what I can tell, there would not be any behavioral difference, and there would be no need for the asymmetric treatment of move and copy operations.

[For people who like citations of the standard, the lack of declaration of move operations for classes with copy operation declarations is specified in 12.8/9 and 12.8/20, and the deleted copy operations for classes with move operation declarations are specified in 12.8/7 and 12.8/18.]

3

3 Answers

8
votes

When a class would be moved but for the fact that no move constructor is declared, the compiler falls back to copy constructor. In the same situation, if move constructor is declared as deleted, the program would be ill-formed. Thus, if move constructor were implicitly declared as deleted, a lot of reasonable code involving existing pre-C++11 classes would fail to compile. Things like myVector.push_back(MyClass())

This explains why move constructor cannot be implicitly declared deleted when copy constructor is defined. This leaves the question of why copy constructor is implicitly declared deleted when move constructor is defined.

I don't know the exact motivation of the committee, but I have a guess. If adding a move constructor to existing C++03-style class were to remove a (previously implicitly defined) copy constructor, then existing code using this class may change meaning in subtle ways, due to overload resolution picking unexpected overloads that used to be rejected as worse matches.

Consider:

struct C {
  C(int) {}
  operator int() { return 42; }
};

C a(1);
C b(a);  // (1)

This is a legacy C++03 class. (1) invokes an (implicitly defined) copy constructor. C b((int)a); is also viable, but is a worse match.

Imagine that, for whatever reason, I decide to add an explicit move constructor to this class. If the presence of move constructor were to suppress the implicit declaration of copy constructor, then a seemingly unrelated piece of code at (1) would still compile, but silently change its meaning: it would now invoke operator int() and C(int). That would be bad.

On the other hand, if copy constructor is implicitly declared as deleted, then (1) would fail to compile, alerting me to the problem. I would examine the situation and decide whether I still want a default copy constructor; if so, I would add C(const C&)=default;

2
votes

Why does this asymmetry exist?

Backward compatibility, and because the relationship between copying and moving is already asymmetrical. The definition of MoveConstructible is a special case of CopyConstructible, meaning that all CopyConstructible types are also MoveConstructible types. That's true because a copy constructor taking a reference-to-const will handle rvalues as well as lvalues.

A copyable type can be initialized from rvalues without a move constructor (it just might not be as efficient as it could be with a move constructor).

A copy constructor can also used to perform a "move" in implicitly-defined move constructors of derived classes when moving the base sub-object.

So a copy constructor can be seen as a "degenerate move constructor", so if a type has a copy constructor it doesn't strictly need a move constructor, it is already MoveConstructible, so simply not declaring the move constructor is acceptable.

The opposite is not true, a movable type is not necessarily copyable, e.g. move-only types. In those cases, making the copy constructor and assignment deleted provides better diagnostics than just not declaring them and getting errors about binding lvalues to rvalue references.

Why not just specify that if a move operation is declared, no copy operations will be declared?

Better diagnostics and more explicit semantics. "Defined as deleted" is the C++11 way to clearly say "this operation is not allowed", rather than just happening to be omitted by mistake or missing for some other reason.

The special case of "not declared" for move constructors and move assignment operators is unusual and is special because of the asymmetry described above, but special cases are usually best kept for a few narrow cases (it's worth noting here that "not declared" can also apply to the default constructor).

Also worth noting is that one of the paragraphs you refer to, [class.copy] p7, says (emphasis mine):

If the class definition does not explicitly declare a copy constructor, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor.

"The latter case" refers to the "otherwise, it is defined as defaulted" part. Paragraph 18 has similar wording for the copy assignment operator.

So the intention of the committee is that in some future version of C++ other types of special member function will also cause the copy constructor and copy assignment operator to be deleted. The reasoning is that if your class needs a user-defined destructor then the implicitly-defined copy behaviour is probably not going to do the right thing. That change hasn't been made for C++11 or C++14 for backward compatibility reasons, but the idea is that in some future version to prevent the copy constructor and copy assignment operator being deleted you will need to declare them explicitly and define them as defaulted.

So deleting copy constructors if they might not do the right thing is the general case, and "not declared" is a special case just for move constructors, because the copy constructor can provide the degenerate move anyway.

0
votes

It is essentially to avoid migrated code to perform unexpected different actions.

Copy and move require a certain level of coherence, so C++11 -if you declare just one- suppress the other.

Consider this:

C a(1); //init
C b(a); //copy
C c(C(1)); //copy from temporary (03) or move(11).

Suppose you write this in C++03.

Suppose I compile it later in C++11. If no ctor are declared, the default move does a copy (so the final behavior is the same as C++03).

If copy is declared, move is deleted, and sine C&& decays into C const& The third statement result in a copy from a temporary. This is still a C++03 identical behavior.

Now, if I'm adding later a move ctor, it means I'm changing the behavior of C (something you did not plan when defining C in C++03), and since a movable object does not need to be copyable (and vice versa), The compiler assumes that by making it movable, the dafault copy may be not anymore adequate. It's up to me to implementing it in coherence with the move or -if I found it adequate- restore the C(const C&)=default;