Evethir
Evethir

Reputation: 41

Mixing Rvalue and LValue reference

I'm trying to better understand LValue, RValue, and how std::move works. I have the following code

#include <string>

class A 
{
public:
A() = default;
A(std::string&& aString): myString(std::move(aString)) {}
std::string myString;
};

class B
{
public: 
void InitMembers(std::string& aString) { myA = A(std::move(aString));}
private:
A myA;
};


int main() 
{
 B b; 
std::string hello; 
b.InitMembers(hello);
}

my questions are:

Isn't the following implementation good as the one above?

#include <string>
class A 
{
public:
A() = default;
A(std::string& aString): myString(std::move(aString)) {}
std::string myString;
};

class B
{
public: 
void InitMembers(std::string& aString) { myA = A(aString);}
private:
A myA;
};

int main() 
{
 B b; 
std::string hello; 
b.InitMembers(hello);
} 

Thanks :)

Upvotes: 1

Views: 206

Answers (1)

Enlico
Enlico

Reputation: 28416

Concerning

A(std::string&& aString): myString(std::move(aString)) {}

std::string&& denotes an rvalue reference to a std::string. Rvalue references only bind to rvalues (both prvalues and xvalues), so the two possible call sites will be like this:

// assuming this is defined somewhere
std::string f(void); // returns by value, i.e. `f()` is an rvalue
// call site 1
A a1{f()}; // `f()` is an rvalue (precisely a prvalue)

// assuming this
std::string s{"ciao"}; // s is an lvalue
// call site 2
A a2{std::move(s)}; // `std::move(s)` is an rvalue (precisely an xvalue)
                    // i.e. with `std::move` we turn s into an rvalue argument,
                    // so it can bind to the rvalue reference parameter
// don't expect s to be the one it was before constructing a2

In either case, what does the constructor do with aString?

Well, aString is an lvalue, because "it has a name" (the actual definition is a bit more complicated, but this is the easiest one to get started with, and it isn't all that wrong after all), so if you use it verbatim, the compiler won't assume it is bound to a temporary and it won't let myString steal it resources.

But you know that aString is bound to a temporary, because you've declared it as std::string&&, so you pass it as std::move(aString) to tell the compiler "treat this is a temporary".

Yes, technically also the compiler knows that aString is bound to a temporary, but it can't std::move it automatically. Why? Because you might want to use it more than once:

A(std::string&& aString) : myString(aString/* can't move this */) {
  std::cout << aString << std::endl; // if I want to use it here
}
// yes, this is a very silly example, sorry

As regards

void InitMembers(std::string& aString) { myA = A(std::move(aString));}

aString denotes an lvalue reference to non-const std::string, so you can pass to .InitMembers only non-const lvalues.

Then inside the function you're std::moveing it to tell A's constructor "look, this is a temporary". But that also means that at the call site (b.InitMembers(hello);) you're leaving the input (hello) in a moved-from state, just like the s in the first example above. That's ok, because the caller knows that InitMembers takes its parameter by non-const lvalue reference, so it is aware that the argument they pass can be changed by the call. Just like in the previous example it's the user who's writing std::move around s, so they're supposed to know what they do.

For more details about how std::move works (and std::forward as well), I want to point you to this answer of mine.

Upvotes: 1

Related Questions