I am struggling to understand implicit move operations when a class has a member whose move operations were not defined:
int main() {
struct A // no move: move = copy
{
A() = default;
A(const A&) {
cout << "A'copy-ctor\n";
};
A& operator=(const A&) {
cout << "A'copy-assign\n";
return *this;
}
};
struct B
{
B() = default;
A a; // does this make B non-moveable?
unique_ptr<int> upi;
// B(B&&) noexcept = default;
// B& operator=(B&&)noexcept = default;
};
A a;
A a2 = std::move(a); // ok use copy ctor instead of move one
a2 = std::move(a); // ok use copy assignment instead of move one
B b;
B b2 = std::move(b); // why this works?
b = std::move(b2); // and this works?
// b = b2; // error: copy deleted because of non-copyable member upi
cout << "\nDone!\n";
}
So what I see is A
is a non-moveable class because of the definition of its copy control operations so it can only be copied and any attempt to move an object of this class, the corresponding copy operation is used instead.
Until here it is OK if i am correct. But B
has a non-copy-able object upi
which is a unique_ptr
thus the copy operations are defined as deleted functions so we cannot copy objects of this class. But this class has a non-move-able object a
thus i think that this class (B
) is neither copy-able nor move-able. But why the initialization of b2
and the assignment of b
works fine? What happens exactly?
B b2 = std::move(b); // ok?!
Why the line above invokes the copy constructor of class A
and does it invoke move constructor of B
?
B
, the initialization above will not compile complaining about referencing a deleted funtion, the same thing for the assignment!Can anyone help me what happens exactly? I have googled and read in cppreference and many websites before posting the question here.
The output:
A'copy-ctor
A'copy-assign
A'copy-ctor
A'copy-assign
Done!
Keep in mind what it means to "move" data in C++ (assuming we follow the usual conventions). If you move object x
to object y
, then y
receives all the data that was in x
and x
is... well, we don't care what x
is as long as it is still valid for destruction. Often we think of x
as losing all of its data, but that is not required. All that is required is that x
is valid. If x
ends up with the same data as y
, we don't care.
Copying x
to y
causes y
to receive all the data that was in x
, and x
is left in a valid state (assuming the copy operation follows conventions and is not buggy). Thus, copying counts as moving. The reason for defining move operations in addition to copy operations is not to permit something new, but to permit greater efficiency in some cases. Anything that can be copied can be moved unless you take steps to prevent moves.
So what I see is
A
is a non-moveable class because of the definition of its copy control operations so it can only be copied and any attempt to move an object of this class, the corresponding copy operation is used instead.
What I see is that A
is a moveable class (despite the lack of move constructor and move assignment), because of the definition of its copy control operations. Any attempt to move an object of this class will fall back on the corresponding copy operation. If you want a class to be copyable but not movable, you need to delete the move operations, while retaining the copy ones. (Try it. Add A(A&&) = delete;
to your definition of A
.)
The B
class has one member that can be moved or copied, and one member that can be moved but not copied. So B
itself can be moved but not copied. When B
is moved, the unique_ptr
member will be moved as you expect, and the A
member will be copied (the fallback for moving objects of type A
).
Things get more worse for me: if I uncomment the lines of move operations in
B
, the initialization above will not compile complaining about referencing a deleted funtion, the same thing for the assignment!
Read the error message more closely. When I replicated this result, the "use of deleted function" error was followed by a note providing more details: the move constructor was deleted because "its exception-specification does not match the implicit exception-specification". Removing the noexcept
keywords allowed the code to compile (using gcc 9.2 and 6.1).
Alternatively, you could add noexcept
to the copy constructor and copy assignment of A
(keeping noexcept
on the move operations of B
). This is one way to demonstrate that the default move operations of B
use the copy operations of A
.