Is returning by rvalue reference more efficient?
Asked Answered
B

2

160

for example:

Beta_ab&& Beta::toAB() const {
    return move(Beta_ab(1, 1));
}
Babism answered 12/7, 2009 at 18:44 Comment(1)
For those who looks for an answer why lifetime of the temporary created on return statement is not extended (because rvalue (T&&) references and const lvalue (const T&) references generally extend lifetime of a temporary), refer to exclusions paragraph on Lifetime of a temporary section on related cppreference pageWit
F
308
Beta_ab&&
Beta::toAB() const {
    return move(Beta_ab(1, 1));
}

This returns a dangling reference, just like with the lvalue reference case. After the function returns, the temporary object will get destructed. You should return Beta_ab by value, like the following

Beta_ab
Beta::toAB() const {
    return Beta_ab(1, 1);
}

Now, it's properly moving a temporary Beta_ab object into the return value of the function. If the compiler can, it will avoid the move altogether, by using RVO (return value optimization). Now, you can do the following

Beta_ab ab = others.toAB();

And it will move construct the temporary into ab, or do RVO to omit doing a move or copy altogether. I recommend you to read BoostCon09 Rvalue References 101 which explains the matter, and how (N)RVO happens to interact with this.


Your case of returning an rvalue reference would be a good idea in other occasions. Imagine you have a getAB() function which you often invoke on a temporary. It's not optimal to make it return a const lvalue reference for rvalue temporaries. You may implement it like this

struct Beta {
  Beta_ab ab;
  Beta_ab const& getAB() const& { return ab; }
  Beta_ab && getAB() && { return move(ab); }
};

Note that move in this case is not optional, because ab is neither a local automatic nor a temporary rvalue. Now, the ref-qualifier && says that the second function is invoked on rvalue temporaries, making the following move, instead of copy

Beta_ab ab = Beta().getAB();
Fakery answered 12/7, 2009 at 19:54 Comment(22)
I had always assumed the dangling reference problem went away automagically when the return type was an r-value reference. Glad I got that straighted out before it bit me. Stack smashing bugs suck.Appel
:) Really, rvalue references are "just references" like lvalue references are. they don't copy or store anything.Fakery
And even if it doesn't elide the constructors, the compiler something knows it can return an rvalue-ref for you automatically safely. For example, according to my experiments, return x; is the same as return std::move(x). (where x is a local variable (i.e. this point I've made doesn't directly apply to the original question about returning a temporary)).Unweave
What's with two ampersands in each signature?Cliff
what does the const & qualifier on a member function more than a simple const ?Lutenist
@Arlen: rvalue references, added in C++11.Kreutzer
+1-ed, but broken link: BoostCon09 Rvalue References 101Bonny
@Lutenist These are ref-qualifiers.Corey
@JohannesSchaub-litb I tried "Beta_ab && getAB() ..." and "Beta_ab getAB() ..." (with and without the && in the return type). They produced the same result. Is this && really necessary?Engage
@NeilG There're two sets of &&, one next to the return type, one at the end of the declaration. The second set means the function should be invoked on a rvalue. I am asking about the first set. Did I miss something?Engage
@CandyChiu I think what Johannes is saying in his answer is that the first set returns a dangling reference. The fact that "it produced the same result" was just an accident.Babism
@NeilG In the second set of code (the correct set without dangling reference), is there a diff between "Beta_ab && getAB() &&" vs " Beta_ab getAB() &&". What I tried with gnu and VS indicates that the first set of && doesn't change the behavior. I want to know in what circumstances does this declaration alters behavior. Thanks.Engage
@candychiu the second one returns by value, which might make a copy. Did you declare a copy constructor and print something when it is called?Babism
@NeilG I removed the copy constructor, and it still works. I printed the content, it was moved. Try this: ideone.com/wpXm73. foo1 and foo2 makes no difference.Engage
@candychiu all classes have a default copy constructor. Did you actually delete it using the new syntax? Please ask another question if you're still confused. I think there are a lot of misunderstandings in your questionsBabism
@NeilG Yes. If you open up the code in the link, the copy constructor and operator= has been explicitly deleted. Do you mind looking at the link? If you can't come up with answer after looking, I can open another question.Engage
@CandyChiu I recommend that you create a new SO question and pose your question there, and asking about something like "diff between && and no ref for &&-qualified functions".Fakery
what if I do this: Beta_ab && Beta::toAB() const { return Beta_ab(1, 1); } Is this correct?Seraphine
@JohannesSchaub-litb I think I understand this, but why can't I prove it dangles? : int&& return_1() { int a{ 1 }; return move(a); // dangling reference? } is working without problem. Do you have an example where it fails?Dunigan
Wait, isn't std::move just a static_cast to rvalue-reference? So if we removed the move from Johannes' example and just said return ab; the function would return an rvalue-reference to Beta_ab initialized with ab, that is, exactly the result of move(ab) no? But in this case, the move would be superfluous, contradicting Johannes' statement that "the move is not optional".Coastal
Is this right? An rvalue reference extends the lifetime of the temporary object to which it refers, and so that temporary value doesn't become dangling immediately. Assuming the user assigns the result of toAb(), then the lifetime is extended until the move-assignment operator completes. Of course, if the user binds the result to a reference, then here be dragons, and I certainly agree that it's better not to return rvalue references since RVO removes the need in a much safer way, but I challenge the dangling-reference argument. Note that std::get() returns an rvalue reference when given one.Pacifa
As a continuation to my comment, I found the following answers interesting: https://mcmap.net/q/152153/-why-does-get-helper-of-std-tuple-return-rvalue-reference-instead-of-value, https://mcmap.net/q/152154/-overloading-on-r-value-references-and-code-duplication (and also the comments by Howard and Aaran)Pacifa
C
-1

It can be more efficient, for example, in a bit different context:

template <typename T>
T&& min_(T&& a, T &&b) {
    return std::move(a < b? a: b);
}

int main() {
   const std::string s = min_(std::string("A"), std::string("B"));
   fprintf(stderr, "min: %s\n", s.c_str());
   return 0;
}

As an interesting observation, on my machine clang++ -O3 generates 54 instructions for code above versus 62 instructions for regular std::min. However, with -O0 it generates 518 instructions for code above versus 481 for regular std::min.

UPDATE

For folks that disagree with or don't understand this answer, I would suggest to play with this sample in Compiler Explorer aka godbolt (https://godbolt.org) first before saying that something is wrong:

// use compiler flags: -O3 --std=c++20 -fno-exceptions
#include <utility>
#include <string>

// Example of the `min(a, b)` implementation that can take advantage of return by rvalue.
template <typename T>
T&& min_(T&& a, T &&b) {
    return std::move(a < b? a: b);
}

// Test class that supports both copy and move construction.
struct Bar {
    Bar();
    Bar(const Bar &other);
    Bar(Bar &&other);
    ~Bar();
    char *ptr;
    friend bool operator <(const Bar& lhs, const Bar& rhs);
};

// Just to tell the optimizer that we care about the value.
void consume(const void *);

#if 1
void with_rvalue() {
   const Bar s = min_(Bar(), Bar());
   consume(&s);
}
#else
void without_rvalue() {
   const Bar s = std::min(Bar(), Bar());
   consume(&s);
}
#endif

In with_rvalue() we get:

        Bar::Bar() [object constructor]
        Bar::Bar() [object constructor]
        operator<(Bar const&, Bar const&)
        Bar::Bar(Bar&&) [move object constructor]
        Bar::~Bar() [object destructor]
        Bar::~Bar() [object destructor]
        consume(void const*)
        Bar::~Bar() [object destructor]

In without_rvalue() we get:

        Bar::Bar() [object constructor]
        Bar::Bar() [object constructor]
        operator<(Bar const&, Bar const&)
        Bar::Bar(Bar const&) [copy object constructor]
        Bar::~Bar() [object destructor]
        Bar::~Bar() [object destructor]
        consume(void const*)
        Bar::~Bar() [object destructor]

So as you can see, when returning by rvalue we avoid an object copy. Practicality of this primitive is another topic, but here you have an example of how returning by rvalue generates more optimal code.

Cephalic answered 9/4, 2018 at 17:15 Comment(8)
I'm confused by your answer. Had tried a similar (maybe) version but failed: ideone.com/4GyUbZ Could you explain why?Sogdian
You used reference on temporary object in for(:) and integrating over deallocated object. Fix: ideone.com/tQVOalCephalic
isn't this answer actually wrong? for template parameter T, T&& is not an r-value reference, but rather a universal reference, and for that case, we should always call std::forward<T>, not std::move! Not to mention, that this answer directly contradicts the top-voted one above.Lukelukens
@Lukelukens this answer is a contrived example how returning by rvalue can be more efficient. std::move() is just used as an explicit cast to illustrate the point more clearly. It's not a code you would copy-paste into your project. It doesn't contradict top-voted answer, because there temporary object is created inside the function. Here returned object is one of the arguments (temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created).Cephalic
@Cephalic then please replace T with std::string; there's no need to use templates at all here, and using T&& as a r-value reference is just awful style that unnecessarily confuses people new to templates and r-values.Lukelukens
I'm surprised so many people are missing the point here. This is not about style or teaching people how to write code. This is an example that illustrates a very specific point - can return by rvalue be better for performance. Expanded the answer with more details.Cephalic
@Lukelukens what is the difference between an r-value reference and a "universal reference"? What is a universal reference? Why do you think you should only call std::forward<T> instead of std::move for it?Prehistory
@Lukelukens Also, there's absolutely no reason to confuse the answer by changing the template to std::string. Your opinion about using T&& is highly debatable, and a culture of hiding syntax like this because it's "confusing" (i.e., refusing to learn about it) is probably the reason so few people understood the answer and downvoted/complained... Honestly, I'm also alarmed at how poorly this answer was received. Its contradiction of the top-voted answer seems to be evidence of the top-voted answer being wrong.Prehistory

© 2022 - 2024 — McMap. All rights reserved.