Discussion: Monsters on the Move
Execute the code to understand the output and gain insights into constructor behavior and value categories.
We'll cover the following
Run the code
Now, it’s time to execute the code and observe the output.
#include <iostream>struct Monster{Monster() = default;Monster(const Monster &other){std::cout << "Monster copied\n";}Monster(Monster &&other){std::cout << "Monster moved\n";}};struct Jormungandr : public Monster{Jormungandr() = default;Jormungandr(const Jormungandr &other) : Monster(other){std::cout << "Jormungandr copied\n";}Jormungandr(Jormungandr &&other) : Monster(other){std::cout << "Jormungandr moved\n";}};int main(){Jormungandr jormungandr1;Jormungandr jormungandr2{std::move(jormungandr1)};}
Understanding the output
When we initialize the second Jormungandr
with a moved-from object, the move constructor gets called for Jormungandr
but the copy constructor for Monster
. Why is that?
As we learned in the Will It Move? puzzle, when we wrap an expression in std::move()
, it turns into an rvalue. An important detail here, though, is that value categories are about expressions, not objects. So, nothing happens to the jormungandr1
object itself; it’s just that the jormungandr1
expression is an lvalue, but the std::move(jormungandr1)
expression is an rvalue.
It would be simpler if it was called “l-expression” and “r-expression” instead of“lvalue” and “rvalue.”
When we then copy-initialize jormungandr2
with the expression Jormungandr jormungandr2{std::move(jormungandr1)}
, overload resolution prefers the move constructor of Jormungandr
over its copy constructor since the expression std::move(jormungandr1)
is an rvalue, which can be moved from. And yes, we read that right; this form is always called copy-initialization, even if the move constructor happens to be called. There’s no such thing as move-initialization.
Fixing the move constructor
Now, let’s turn our attention to the implementation of the move constructor of Jormungandr
. You might already have spotted the bug: we forgot to wrap other
in a std::move()
like so:
Jormungandr(Jormungandr &&other) : Monster(std::move(other))
The other
parameter is an rvalue reference. But the other
expression, which we pass to the Monster
constructor, is an lvalue! Since the other
expression is an lvalue, we can’t use the move constructor, and overload resolution picks the copy constructor instead. However, after wrapping it in std::move()
, the expression std::move(other)
is now an rvalue, and the move constructor is picked.
#include <iostream>struct Monster{Monster() = default;Monster(const Monster &other){std::cout << "Monster copied\n";}Monster(Monster &&other){std::cout << "Monster moved\n";}};struct Jormungandr : public Monster{Jormungandr() = default;Jormungandr(const Jormungandr &other) : Monster(other){std::cout << "Jormungandr copied\n";}Jormungandr(Jormungandr &&other) : Monster(std::move(other)){std::cout << "Jormungandr moved\n";}};int main(){Jormungandr jormungandr1;Jormungandr jormungandr2{std::move(jormungandr1)};}
The output would then instead be the following:
Monster movedJormungandr moved
What would happen if Monster
was defined like this instead?
struct Monster
{
Monster() = default;
Monster(const Monster &other)
{
std::cout << "Monster copied\n";
}
};
Verify your answer by executing the following code:
#include <iostream>struct Monster{Monster() = default;Monster(const Monster &other){std::cout << "Monster copied\n";}};struct Jormungandr : public Monster{Jormungandr() = default;Jormungandr(const Jormungandr &other) : Monster(other){std::cout << "Jormungandr copied\n";}Jormungandr(Jormungandr &&other) : Monster(other){std::cout << "Jormungandr moved\n";}};int main(){Jormungandr jormungandr1;Jormungandr jormungandr2{std::move(jormungandr1)};}
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.