I'm currently learning about R-value references by reading tutorials. Many tutorials mention move constructors/assignment operators as the main use case of R-value references. So I'm wondering whether/how they should be used outside of the "Rule of 5".
Say I have a function
std::string foo();
which returns a potentially large string, and want to pass its output to
int bar (const std::string& s);
.
Consider 3 options of doing this.
1.
const std::string my_string = foo();
const int x = bar(my_string);
std::string&& my_string = foo();
const int x = bar(my_string);
const int x = bar(foo());
My understanding is that 2. and 3. are essentially the same (in both cases, there's never an l-value holding my_string
), but 2. may be more readable (the string can be given a name, and the statement can be broken up into multiple lines). None of the 3 options does any unnecessary copies. The difference between 2. and 1. is that in 1., my_string
will live till it goes out of scope, while in 2., I tell the compiler that I don't need my_string
any more after passing it to bar
, allowing to free its memory earlier.
Is the above correct? If so, is 2. the best option to use here?
CodePudding user response:
My understanding is that 2. and 3. are essentially the same
No, in fact 1. and 2. are essentially the same, but 3. is different.
In both 1. and 2. my_string
is an lvalue, since names of variables are always lvalues. In both cases the string object lives until the end of the scope. In case of 1. because that is the scope of the object and in 2. because of the lifetime extension rules on reference binding.
In 3. foo()
is a prvalue (a kind of rvalue). The temporary object materialized from it and to which the reference parameter of the function binds lives until the end of the initialization of x
.
So if bar
had different overloads taking lvalue references or rvalue references, in 1. and 2. the lvalue reference overload would be chosen and in 3. the rvalue reference overload.
Since C 17, none of the three methods make unnecessary copies. There will be only one std::string
object, either the temporary one in case of 2. and 3. or the named one in case of 1.
Before C 17, there could be an additional copy in case of 1. to copy from the return value of foo
into my_string
. However the compiler was allowed to elide this copy and that probably almost always happened in practice. Since C 17 this elision is mandatory.
while in 2., I tell the compiler that I don't need my_string any more after passing it to bar,
Aside from this applying to 3., not 2., the compiler actually doesn't care about this at all, the lifetime of the object is not affected.
It is the function you are passing to that cares. If a function takes an rvalue reference, by convention, that means that the function is allowed to use and modify the object's state in whatever way it suits it. Your function doesn't have a rvalue overload, so it doesn't matter how you pass the string to it. bar
should never modify the state of the object.
So to make the distinction relevant to your case, you would either add an overload
int bar(std::string&&);
that will be called instead of the first overload for 3. and somehow makes use of the implied permission to put the string object into an unspecified state, or you would use a single overload
int bar(std::string);
in which case before C 17 the parameter will be constructed through the copy constructor for 1. and 2. or the move constructor for 3. The move constructor makes use of the rvalue convention and will reuse the passed string's allocations. Or in case of 3., since C 17 definitively and optionally before that, the copy/move is elided and the parameter is directly constructed by the function foo()
.