3

After replying on question about returning temporaries I've noticed there was a second reply which is slightly different.

Instead of returning by value, it returned by rvalue reference. Can you explain what the difference is between these approaches and where the risks are for them?

struct Bar
    {
    Bar& doThings() & {return *this;}

     // Returning rvalue reference
    Bar&& doThings() &&    {return std::move(*this);}

     // Alternative: Returning by value
    //Bar doThings() && {return std::move(*this);}

    std::unique_ptr<int> m_content; // A non-copyable type
    };
xskxzr
  • 12,442
  • 12
  • 37
  • 77
JVApen
  • 11,008
  • 5
  • 31
  • 67
  • That's because [`std::unique_ptr`](https://en.cppreference.com/w/cpp/memory/unique_ptr) doesn't implement copy behavior. It can only be moved. – πάντα ῥεῖ Jul 07 '18 at 07:35
  • For this case it doesn't matter, since in the callee the returned value will be an rvalue anyway. – Some programmer dude Jul 07 '18 at 07:37
  • 1
    Possible duplicate of: https://stackoverflow.com/questions/1116641/is-returning-by-rvalue-reference-more-efficient – Paul Sanders Jul 07 '18 at 07:55
  • If caller does `doThings()` without using the return value; then the third version will destroy the mangaed object whereas the first two will not – M.M Jul 07 '18 at 08:30
  • @πάνταῥεῖ I was aware about that, the question is explicitly about the difference in return type for the `doThings() &&` method compared to the commented one. – JVApen Jul 07 '18 at 09:15
  • @PaulSanders All the explanation in that question is about the returning of a newly created instance. However, in this case, the instance already existed. – JVApen Jul 07 '18 at 09:20
  • OK, but is it wise to _move_ out of `*this`? – Paul Sanders Jul 07 '18 at 09:26
  • @PaulSanders Probably no, however whether it's this or a member doesn't really matter I think. All better than deep copying without it being needed. – JVApen Jul 07 '18 at 09:29
  • Isn't it something like. Create an lvalue and assign it to a new variable (possible copy nonetheless) vs create an rvalue and keep it rvalue until assignment (guaranteed rvalue) ? – Volt Jul 07 '18 at 09:56

2 Answers2

3

One major difference is that, if rvalue reference is returned, the lifetime of the temporary will not be extended when the return value is bound to a reference.

For example,

Bar&& rb = Bar{}.doThings();
use(rb); // not safe if returning rvalue reference, while safe if returning by value
xskxzr
  • 12,442
  • 12
  • 37
  • 77
  • That would be a temporary created by `std::move`? (Just to clarify things in my own mind) – Paul Sanders Jul 07 '18 at 15:47
  • 1
    `std::move` itself will not create temporary. If returning by value, then a new temporary (as return value) is created and initialized from `std::move(*this)`. The lifetime of the new temporary is extended in my example. – xskxzr Jul 07 '18 at 15:49
  • @xskxzr When `do_things()` return a rvalue-reference, why does the `Bar&& rb = ...` not extend the lifetime? I browsed https://en.cppreference.com/w/cpp/language/reference_initialization and under "Lifetime of a temporary" there are 4 bullet points and one last statement: "In general, the lifetime of a temporary cannot be further extended by "passing it on": a second reference, initialized from the reference to which the temporary was bound, does not affect its lifetime. " Does this apply here? Or is it bullet point 3? – phön Jul 20 '18 at 08:50
  • @phön Right, this is exactly the "second reference" case. – xskxzr Jul 20 '18 at 08:54
  • @xskxzr Is the reference returned by `std::move(*this)` considered the first one, and the reference returned by the `doThings` function considered the second one and the final `Bar&& rb` considered the third? Technically, could copy elision and RVO be applied and let `Bar&& rb` be the first and only reference in this snippet? From a technical standpoint, it seems like lifetime extention is possible in this example, its just against "the current rules", or? – phön Jul 20 '18 at 09:12
  • @phön I'm not an expert of compiler, but I think yes, it is technically possible to extend the lifetime, and it's just against the current rules. – xskxzr Jul 20 '18 at 09:38
  • @xskxzr Thanks for your insights. – phön Jul 20 '18 at 09:50
0

I was interested in this, so I knocked up a little test program:

#include <memory>
#include <iostream>

struct Bar
{
    // Return by reference
    Bar& doThings() & { return *this; }

    // Return by rvalue reference
    Bar&& doThings() && { std::cout << "in Bar&& doThings, this=" << (void *) this << "\n"; return std::move (*this); }

    ~Bar () { std::cout << "Bar destroyed at " << (void *) this << "\n"; }

    int first;
    std::unique_ptr<int> m_content; // Make Bar a non-copyable type
};

int main ()
{
    std::cout << std::hex;
    Bar bar;
    std::cout << "bar is at " << &bar << "\n";
    Bar& bar_ref = bar.doThings ();
    std::cout << "bar_ref refers to " << &bar_ref.first << "\n\n";
    Bar&& bar_rvalue_ref = Bar ().doThings ();
    std::cout << "bar_rvalue_ref refers to " << &bar_rvalue_ref.first << "\n\n";
}

Output:

bar is at 0x7ffdc10846f0
bar_ref refers to 0x7ffdc10846f0

in Bar&& doThings, this=0x7ffdc1084700
Bar destroyed at 0x7ffdc1084700
bar_rvalue_ref refers to 0x7ffdc1084700

Bar destroyed at 0x7ffdc10846f0

So this tell us that neither form of doThings() creates a temporary. You can also verify this at Godbolt.

So, looking at the implementation of Bar as written, where does this leave us? Well, I personally can't see any purpose in Bar&& doThings() at all because:

  1. AFAICT, you can only call it on a newly constructed object so there seems little point in it anyway.
  2. Because it moves from *this, it eats, in principle, that newly constructed object so the reference returned (which still refers to it) is not to be relied on.
  3. The temporary created in step 1 has an infinitessimal lifetime, as @xskxzr has already pointed out.

Whereas, Bar& doThings() works just fine (and, returns, in effect, a pointer to the object itself, so no copying or temporaries will ever be involved).

I'm sure I've missed something important so I'd love to hear people's reactions to this. Surely there is some purpose in returning by rvalue reference, perhaps in more complicated scenarios. Thanks.

Live demo

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
Paul Sanders
  • 24,133
  • 4
  • 26
  • 48