Does it make sense for a function to return an rvalue reference?
Asked Answered
c++
L

3

29

What would be a valid use case for a signature like this?:

T&& foo();

Or is the rvalue ref only intended for use as argument?

How would one use a function like this?

T&& t = foo(); // is this a thing? And when would t get destructed?
Libertine answered 2/5, 2019 at 19:22 Comment(4)
std::move comes to mind. It certainly returns T&&. Edit : std::optional::value also has an T&& overload. Edit 2 : It also has a const T && overload, though I'll admit I don't understand the meaning.Overhaul
@FrançoisAndrieux the std::get family of functions, too.Botanical
See this answer.Bacchanalia
Isn't that a forwarding reference?Newsstand
I
27

For a free function it doesn't make much sense to return a rvalue reference. If it is a non-static local object then you never want to return a reference or pointer to it because it will be destroyed after the function returns. It can possibly make sense to return a rvalue reference to an object that you passed to the function though. It really depends on the use case for if it makes sense or not.

One thing that can greatly benefit from returning an rvalue reference is a member function of a temporary object. Lets say you have

class foo
{
    std::vector<int> bar;
public:
    foo(int n) : bar(n) {}
    std::vector<int>& get_vec() { return bar; }
};

If you do

auto vec = foo(10).get_vec();

you have to copy because get_vec returns an lvalue. If you instead use

class foo
{
    std::vector<int> bar;
public:
    foo(int n) : bar(n) {}
    std::vector<int>& get_vec() & { return bar; }
    std::vector<int>&& get_vec() && { return std::move(bar); }
};

Then vec would be able to move the vector returned by get_vec and you save yourself an expensive copy operation.

Indreetloire answered 2/5, 2019 at 19:34 Comment(19)
@FrançoisAndrieux That's covered by my It can possibly make sense to return a rvalue reference to an object that you passed to the function though. It really depends on the use case for if it makes sense or not. catch all. I know there are cases but I really didn't want to try and list them all.Indreetloire
Is there a reason to prefer returning by rvalue-ref compared to returning by value here?Darsey
@Darsey I take it your talking about the get_vec case? If you return by value you incur 2 move operations. Passing by rvalue reference you only have 1 move.Indreetloire
But in get_vec()&& you're returning an rvalue reference to what might be a temporary object (e.g. if used with a materialized prvalue). It would be much safer (and is recommended) to return by value. Any decent compiler will elide the extraneous constructor calls.Transmissible
@CruzJean You can only call get_vec() && on a temporary object so it's safe to call it.Indreetloire
@Indreetloire Well, you can call it on any rvalue (e.g. std::move(a).get_vec()). My point is that you're potentially returning a reference to an object that's about to be destroyed. It's the same problem as returning a reference to a local function variable.Transmissible
std::vector<int> get_vec() && { return std::move(bar); } in this case ends up being better 999/1000. Can you come up with a better example?Wiggs
@MiljenMikic Those ampersands were not redundant. If they are removed it makes the code pointless.Indreetloire
@Indreetloire Notice what @Yakk wrote, std::vector<int> get_vec() &&, not std::vector<int>&& get_vec() &&.Novgorod
@MiljenMikic I've noted and left it as is. I can't come up with a better example so I'm leaving it as is. If you compile the code pre C++17, with copy elision turned off, it will make a difference. It's pretty much moot now but it is still a valid example.Indreetloire
@Indreetloire Yup, in that case it will make a difference. Fair enough.Novgorod
@NathanOliver, I have just ran in real troubles with std::vector<int>&& get_vec() &&. My code is: auto &&v = std::move(a).get_vec(); auto && should be always safe but in this case it will capture only reference and does not prolong lifetime of a. See e.g. #10191177Borstal
@JarekC That question asks about temporaries. You don't have a temporary though, you have an lvalue. Also, no move actually happens here, a still exists, so v is still a valid reference to as internal vector.Indreetloire
@JarekC This overload is really only intended to be used like auto bar = foo{42}.get_vec();Indreetloire
Don't want to make this list too long, @Indreetloire is right, my "failing" case is more similar to auto &&bar = foo{42}.get_vec(); than to my example with move above.Borstal
@CruzJean, similar comment as to the answer below: If we "should not" return &&, why e.g. std::optional is doing exactly this in operator*()?Borstal
@JarekC It is to give you a way to extract the value from the optional without having to make a copy. You can do something like std::optional<expensive> bar() { ... } auto foo = *bar() and now the wrapped expensive object is moved instead of copied.Indreetloire
If std::optional::operator* was T operator*() &&, then auto foo = *bar() would do two moves (possibly optimizable), still no copy.Borstal
@JarekC Two moves is still twice the work. Once of C++ core tenents is high performance.Indreetloire
W
6
T&& t = foo(); // is this a thing? And when would t get destructed?

An rvalue reference is really similar to a lvalue reference. Think about your example like it was normal references:

T& foo();

T& t = foo(); // when is t destroyed?

The answer is that t is still valid to use as long as the object is refers to lives.

The same answer still applies to you rvalue reference example.


But... does it make sense to return an rvalue reference?

Sometimes, yes. But very rarely.

consider this:

std::vector<int> v = ...;

// type is std::tuple<std::vector<int>&&>
auto parameters = std::forward_as_tuple(std::move(v));

// fwd is a rvalue reference since std::get returns one.
// fwd is valid as long as v is.
decltype(auto) fwd = std::get<0>(std::move(parameters));

// useful for calling function in generic context without copying
consume(std::get<0>(std::move(parameters)));

So yes there are example. Here, another interesting one:

struct wrapper {

    auto operator*() & -> Heavy& {
        return heavy;
    }

    auto operator*() && -> Heavy&& {
        return std::move(heavy);
    }

private:
    Heavy instance;
};

// by value
void use_heavy(Heavy);

// since the wrapper is a temporary, the
// Heavy contained will be a temporary too. 
use_heavy(*make_wrapper());
Wrote answered 2/5, 2019 at 19:35 Comment(3)
In the last example, if the wrapper instance you use this on is a temporary, operator*() returns a reference to instance, which is likewise a temporary. So after the function returns you have a reference to an object whose lifetime has ended (undefined behavior).Transmissible
Are you sure you don't need decltype(auto) fwd = std::get<0>(std::move(parameters));?Wiggs
@CruzJean, I agree with you but on the other hand, I really don't understand why e.g. T&& std::optional<T>::operator*() && is defined to behave exactly as proposed by @GuillaumeRacicot. I would expect T std::optional<T>::operator*() && but it is not this way...Borstal
E
1

I think a use case would be to explicitly give permission to "empty" some non-local variable. Perhaps something like this:

class Logger
{
public:
    void log(const char* msg){
        logs.append(msg);
    }
    std::vector<std::string>&& dumpLogs(){
        return std::move(logs);
    }
private:
    std::vector<std::string> logs;
};

But I admit I made this up now, I never actually used it and it also can be done like this:

std::vector<std::string> dumpLogs(){
    auto dumped_logs = logs;
    return dumped_logs;
}
Ergot answered 2/5, 2019 at 19:32 Comment(4)
That's what the other answers have come up with as well, but in the case where this is called on a temporary object, by the time the function returns you have an (rvalue) reference to an object whose lifetime has ended (undefined behavior).Transmissible
@CruzJean Well, my answer was here first and no one downvoted/complained yet so I will keep it here. I am not returning any reference to a temporary anywhere.Ergot
return std::move(logs); where the return value is a reference type and *this is a temporary (due to being an rvalue method) makes this->logs a temporary as well. That's the reference to a temporary I mean.Transmissible
@CruzJean Do you mean e.g. auto&& x = Logger{}.dumpLogs(); ? Yes, that is dangling reference, but that happens for all getters no matter what type of reference they return. My use case would be something like Logger l; /*calls to log()*/, auto logs = l.dumpLogs();/*log again.. and repeat.*/Ergot

© 2022 - 2024 — McMap. All rights reserved.