In the following program, how is it possible for the move to complete if the parameter was passed by value?
Both constructor signatures; MyContainer(Myclass&& myclass)
and MyContainer(Myclass myclass)
work equally well, but Myclass
only has a move constructor, the rest are deleted.
#include <iostream>
class Myclass {
public:
Myclass(int value) : value{value} {std::cout << "Constructed with value " << value << "\n";}
Myclass(Myclass&& other) : value{other.value}
{
other.value = 0;
std::cout << "Move constructed with value " << value << "\n";
}
Myclass(const Myclass& other) = delete;
Myclass& operator=(const Myclass& other) = delete;
Myclass& operator=(Myclass&& other) = delete;
~Myclass() {std::cout << "Destructed with value " << value << "\n";}
private:
int value;
};
class MyContainer {
public:
//MyContainer(Myclass&& myclass) : myclass{std::move(myclass)} { } // Shouldn't this be the constructor definition?
MyContainer(Myclass myclass) : myclass{std::move(myclass)} { } // this also works, why?
private:
Myclass myclass;
};
int main()
{
Myclass myclass{5};
MyContainer mycontainer{std::move(myclass)};
return 0;
}
When the constructor is called I do use std::move
, but how does the compiler 'remember' that it was moved if the parameter was Myclass
and not Myclass&&
?
In both cases you are casting your lvalue myclass
to an rvalue reference.
The return type of std::move
is an rvalue reference, so by definition, the std::move
expression is an rvalue - and as such, can bind to the rvalue reference parameter of the move constructor of MyClass
.
The std::move
instruction can be interpreted as 'I'm done with this variable, it can now be moved'. Note that this is the case irrespective of what the type of the variable being passed to std::move
is - it works just as well with normal variables, lvalue reference and rvalue references.
When you write MyContainer mycontainer{std::move(myclass)};
, we have to enumerate all the viable constructors of MyContainer
that can be called with an xvalue of type Myclass
. MyContainer
has a single relevant constructor which takes an argument of type Myclass
. To see if that constructor is viable, we effectively perform overload resolution again on constructing a Myclass
with an xvalue of type Myclass
. That gives us three constructors:
Myclass(int ); // not viable: no conversion from `Myclass` to `int`
Myclass(Myclass&& ); // viable
Myclass(const Myclass& ) = delete; // viable
The move constructor is viable because you can bind an rvalue reference to an xvalue. The copy constructor is viable because you can bind a const lvalue reference to anything. One of the tiebreakers in overload resolution is picking the least cv-qualified reference. The move constructor is an rvalue reference to Myclass
. The copy constructor is an lvalue reference to Myclass const
- that is more cv-qualified, hence it is less prefered. Thus, we select the move constructor. This constructor is not deleted, so that call is well-formed.
The important part here is that we're constructing with an xvalue of type Myclass
(that's what std::move()
does - it's a cast to xvalue). If we were constructing with an lvalue (i.e. simply MyContainer mycontainer(myclass)
) then only the copy constructor would be viable, but it's explicitly deleted so the entire expression is ill-formed.
how does the compiler 'remember' that it was moved if the parameter was
Myclass
and notMyclass&&
?
There's no remembering. In both cases, we're trying to initialize the argument in the constructor from an xvalue of type Myclass
. In the former case, that involves invoking the move constructor of Myclass
. In the latter case, that's a direct reference binding.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With