Steve Lorimer
Steve Lorimer

Reputation: 28659

Is there any reason to capture a return-value as an rvalue-reference?

I have a move-only struct Foo, and a function Foo get();

If I want to capture the return value of get in a mutable manner, I have two options:

When I capture by rvalue-reference I create an lvalue, much like capturing by value.

I'm struggling to see the point between the different options?

Working example:

#include <iostream>

struct Foo
{
    Foo(std::string s) : s(std::move(s)) {}
    Foo(Foo&& f)       : s(std::move(f.s)) {}

    Foo(const Foo&) = delete;
    Foo& operator=(const Foo&) = delete;

    std::string s;
};

Foo get()
{
    return Foo { "hello" };
}

int main()
{
    // capture return value as l-value
    Foo lv1 = get();

    // move into another lvalue
    Foo lv2 = std::move(lv1);
    std::cout << lv2.s << '\n';

    // capture return value as r-value reference
    Foo&& rv1 = get();

    // move into another lvalue
    Foo lv3 = std::move(rv1);
    std::cout << lv3.s << '\n';

    return 0;
}

Upvotes: 5

Views: 757

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

Foo&&

this creates a reference to a temporary value (or stores a rvalue reference assigned to it). If it stores a reference to a temporary value, its lifetime extends the value.

Foo

This stores a copy of the value, regardless of what was returned.

In C++11 and 14, if Foo cannot be moved, assigning Foo make_foo() to a variable of type Foo is illegal. There is a move there, even if the move is elided (and the return value and the value in the outer scope have merged lifetimes).

In C++17, guaranteed elision means that no move constructor need exist.

Foo x = make_foo(); // Foo make_foo()

the above in C++17 guarantees that the return value of make_foo() is just named x. In fact, a temporary within make_foo may also be x; the same object, with different names. No move need occur.

There are a few other subtle differences. decltype(x) will return the declared type of x; so Foo or Foo&& depending.

Another important difference is its use with auto.

auto&& x = some_function();

this creates a reference to anything. If some_function returns a temporary, it binds an rvalue reference to it and extends its lifetime. If it returns a reference, x matches the type of the reference.

auto x = some_function();

this creates a value, which may be copied from what some_function returns, or may be elided with the return value of some_function if it returns a temporary.

auto&& means, in a sense, "just make it work, and don't do extra work", and it could deduce to a Foo&&. auto means "store a copy".

In the "almost always auto" style, these will be more common than an explicit Foo or Foo&&.

auto&& will never deduce to Foo, but can deduce to Foo&&.

The most common uses of auto&&, even outside of almost always auto, would be:

for(auto&& x : range)

where x becomes an efficient way to iterate over the range, where we don't care what type range has much. Another common use is:

[](auto&& x){ /* some code */ }

lambdas are often used in contexts where the type is obvious and not worth typing again, like being passed to algorithms or the like. By using auto&& for parameter types we make the code less verbose.

Upvotes: 4

Nicol Bolas
Nicol Bolas

Reputation: 473322

Foo lv1 = get();

This requires that Foo is copy/moveable.

Foo&& rv1 = get();

This does not (at least, not as far as this line of code is concerned; the implementation of get may still require one).

Even though compilers are permitted to elide the copy of the return value into the variable, copy initialization of this form still requires the existence of an accessible copy or move constructor.

So if you want to impose as few limitations on the type Foo as possible, you can store a && of the returned value.

Of course, C++17 changes this rule so that the first one doesn't need a copy/move constructor.

Upvotes: 5

Related Questions