1

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:

  • In void InitMembers(string& aString) { myA = A(std::move(aString));} I understand that I have to use std::move to aString in order to cast aString from an LValue reference to a RValue reference. But I have some doubts regarding the meaning of aString in the InitMember scope. aString is provided as an LValue reference, but in the method scope it's considered as an LValue and that's why I have to use the std::move? Std::move should rely on reference deduction (right?), how does it deduce the type in this case? It will deduce a type "string" o "string&" since aString is provided as a LValue Reference in the method's arguments?
  • Why do I have to use std::move also in the constructor initializer of A? Shouldn't be enough the fact that aString is an RValue reference, which triggers the move constructor?

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 :)

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
Evethir
  • 31
  • 3
  • Seems a bit similar to: https://stackoverflow.com/a/31213816/754407, "if it has a name, it is an lvalue reference". Looking at `A` class ctor: you must use `std::move`, as the value is named, it could be used multiple times, thus you must be explicit about it. `std::move` is an "rvalue cast", but the type deduction only hits in when using templated type `T&&`, then it can bind to either type of reference, and `std::move` will act ok with both. – hauron Oct 12 '21 at 07:57
  • "Why do I have to use std::move also in the constructor initializer of A? " -- because `string&& aString ` is a 'reference to an r-value', by itself it's an l-value. – Tharsalys Oct 12 '21 at 07:58
  • hi @Tharsalys you hit one of my doubts, why it's an LValue? I mean, am I right if I say that "aString" is provided as an RValue reference, but it's in fact an LValue? Are RValue references considered as Lvalue in the scope (which in this case it's the initialization list)? – Evethir Oct 12 '21 at 08:13
  • @Evethir, `aString` is an lvalue because "it is a thing with a name". – Enlico Oct 12 '21 at 08:15
  • hi @hauron so in this case std::move won't use reference deduction and will consider the provided type a "std::string" even if "aString" is a LValue reference in "B::InitMembers"? – Evethir Oct 12 '21 at 08:15
  • 1
    Hi @TedLyngmo I've cleaned up the code, I hope it's clearer now :) – Evethir Oct 12 '21 at 08:15
  • @Evethir there is no need for deduction - you are using `string&&`, it will be a `string&&`. If you used `template void foo(T&& bar)`, this is when the deduction could kick it - you could call `foo` with either lvalue or rvalue ref. – hauron Oct 12 '21 at 08:17
  • @Evethir, you might find [this](https://stackoverflow.com/a/65446786/5825294) "indirectly" useful. – Enlico Oct 12 '21 at 08:18

1 Answers1

1

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.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • HI Enlico, Thank's for the awesome answer, now I have a clearer idea of how to use RValue and LValue references. thanks a lot! I've just another question for you. In the second case that I've reported, in whch aString is A constructor is an LValue reference, the std::move operator will still convert it to an RValue reference and I should still be able to steal its data right? – Evethir Oct 13 '21 at 07:57
  • @Evethir, are you referring to `void InitMembers(std::string& aString) { myA = A(std::move(aString));}`? – Enlico Oct 13 '21 at 09:22
  • @Evethir, what about accepting? :P – Enlico Oct 04 '22 at 05:58