Discussion: Will It Move?
Execute the code to understand the output and gain insights into move constructors.
Run the code
Now, it’s time to execute the code and observe the output.
#include <iostream>struct Member{};struct WillItMove{WillItMove() = default;WillItMove(WillItMove &&) = default;const Member constMember_{};};int main(){WillItMove objectWithConstMember;WillItMove moved{std::move(objectWithConstMember)};std::cout << "It moved!\n";}
The WillItMove
has a const member. We can’t move from a const
object, so why could we still move from objectWithConstMember
?
Understanding the output
In this puzzle, we first initialize WillItMove objectWithConstMember;
. Then, on line 17, we initialize a new WillItMove moved{std::move(objectWithConstMember)};
. Since WillItMove
has no copy constructor, the only alternative to initialize moved
is the move constructor. But how can the move constructor perform a move operation from the const Member constMember
?
The move constructor
Let’s start digging from the outside in.
First, what does std::move(objectWithConstMember)
do? The std::move()
doesn’t actually move anything; it just turns the lvalue objectWithConstMember
into an rvalue. Then, this rvalue is used to initialize moved
.
Lvalues vs. rvalues
Lvalues vs. rvalues is a way to categorize expressions.
Lvalue
An lvalue is an expression that evaluates to an actual object that has an identity and cannot be moved from. For instance, the objectWithConstMember
expression evaluates to the objectWithConstMember
object. It has a name and a place in memory, and others could still be using it, so the type system doesn’t allow us to move from it. Traditionally, lvalues have tended to appear to the left of an assignment, hence the name lvalue (left-value).
Rvalue
An rvalue, on the other hand, is an expression that evaluates to something that can be moved from, for instance, a temporary that no one else can get to anyway. Rvalues tend to appear to the right of an assignment, hence the name rvalue (right-value).
The std::move()
turns an lvalue into an rvalue and is the way we tell the type system that “this is fine, you can go ahead and move from this lvalue; I won’t be using it anymore.” With the introduction of std::move()
, it has become more common to see lvalues appear to the right of the assignment inside a std::move()
, so the “left-value” vs. “right-value” mnemonic isn’t quite as useful as it used to be.
Note that even though we initialize moved
from an rvalue, that doesn’t mean we need to use the move constructor to initialize it! We just need any constructor capable of binding to an rvalue. A const lvalue reference can bind to an rvalue, so a copy constructor could also have been used if we had one. We don’t, however, so the move constructor of WillItMove
is our only choice.
Defaulted move constructor
The move constructor of WillItMove
has been explicitly defaulted by the WillItMove(WillItMove &&) = default;
line, so let’s turn our attention to what a defaulted move constructor does. How can it move from a const member?
The explicitly defaulted move constructor direct-initializes all subobjects with the corresponding member from the source object. Essentially, it’s defined something like this:
Class(Class &&other) :BaseClass1{std::move(other)},BaseClass2{std::move(other)},/* ... */member1_{std::move(other).member1_},member2_{std::move(other).member2_}/* ... */{}
So for our WillItMove
class, it looks something like this:
WillItMove(WillItMove && other) :constMember_{std::move(other).constMember_}{}
Handling const members in the move constructors
Again, even if std::move(other).constMember_
is an rvalue, we don’t necessarily need to use a Member
move constructor to initialize constMember_
. Either a copy or a move constructor will do, as long as it can bind to std::move(other).constMember_
. Since we haven’t declared any special member functions for Member
, it implicitly gets both a defaulted copy constructor and a defaulted move constructor. Can one of those bind to the const rvalue other.constMember_
?
Copy vs. move constructor for const members
The implicitly defaulted move constructor for Member
is declared like this:
Member(Member&&)
An rvalue reference to non-const cannot bind to the const rvalue, so we can’t use the move constructor to direct-initialize constMember_
.
The implicitly defaulted copy constructor for Member
is declared like this:
Member(const Member&)
An lvalue reference to const can bind to a const rvalue, so we can use the copy constructor to direct-initialize constMember_
.
This is why the explicitly defaulted move constructor for WillItMove
uses the Members
copy constructor rather than the Members
move constructor to initialize constMember_
.
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.