Pass by value vs pass by rvalue reference

27,536

Solution 1

The rvalue reference parameter forces you to be explicit about copies.

Yes, pass-by-rvalue-reference got a point.

The rvalue reference parameter means that you may move the argument, but does not mandate it.

Yes, pass-by-value got a point.

But that also gives to pass-by-rvalue the opportunity to handle exception guarantee: if foo throws, widget value is not necessary consumed.

For move-only types (as std::unique_ptr), pass-by-value seems to be the norm (mostly for your second point, and first point is not applicable anyway).

EDIT: standard library contradicts my previous sentence, one of shared_ptr's constructor takes std::unique_ptr<T, D>&&.

For types which have both copy/move (as std::shared_ptr), we have the choice of the coherency with previous types or force to be explicit on copy.

Unless you want to guarantee there is no unwanted copy, I would use pass-by-value for coherency.

Unless you want guaranteed and/or immediate sink, I would use pass-by-rvalue.

For existing code base, I would keep consistency.

Solution 2

What do rvalue usages say about an interface versus copying? rvalue suggests to the caller that the function both wants to own the value and has no intention of letting the caller know of any changes it has made. Consider the following (I know you said no lvalue references in your example, but bear with me):

//Hello. I want my own local copy of your Widget that I will manipulate,
//but I don't want my changes to affect the one you have. I may or may not
//hold onto it for later, but that's none of your business.
void foo(Widget w);

//Hello. I want to take your Widget and play with it. It may be in a
//different state than when you gave it to me, but it'll still be yours
//when I'm finished. Trust me!
void foo(Widget& w);

//Hello. Can I see that Widget of yours? I don't want to mess with it;
//I just want to check something out on it. Read that one value from it,
//or observe what state it's in. I won't touch it and I won't keep it.
void foo(const Widget& w);

//Hello. Ooh, I like that Widget you have. You're not going to use it
//anymore, are you? Please just give it to me. Thank you! It's my
//responsibility now, so don't worry about it anymore, m'kay?
void foo(Widget&& w);

For another way of looking at it:

//Here, let me buy you a new car just like mine. I don't care if you wreck
//it or give it a new paint job; you have yours and I have mine.
void foo(Car c);

//Here are the keys to my car. I understand that it may come back...
//not quite the same... as I lent it to you, but I'm okay with that.
void foo(Car& c);

//Here are the keys to my car as long as you promise to not give it a
//paint job or anything like that
void foo(const Car& c);

//I don't need my car anymore, so I'm signing the title over to you now.
//Happy birthday!
void foo(Car&& c);

Now, if Widgets have to remain unique (as actual widgets in, say, GTK do) then the first option cannot work. The second, third and fourth options make sense, because there's still only one real representation of the data. Anyway, that's what those semantics say to me when I see them in code.

Now, as for efficiency: it depends. rvalue references can save a lot of time if Widget has a pointer to a data member whose pointed-to contents can be rather large (think an array). Since the caller used an rvalue, they're saying they don't care about what they're giving you anymore. So, if you want to move the caller's Widget's contents into your Widget, just take their pointer. No need to meticulously copy each element in the data structure their pointer points to. This can lead to pretty good improvements in speed (again, think arrays). But if the Widget class doesn't have any such thing, this benefit is nowhere to be seen.

Hopefully that gets at what you were asking; if not, I can perhaps expand/clarify things.

Solution 3

Unless the type is a move-only type you normally have an option to pass by reference-to-const and it seems arbitrary to make it "not part of the discussion" but I will try.

I think the choice partly depends on what foo is going to do with the parameter.

The function needs a local copy

Let's say Widget is an iterator and you want to implement your own std::next function. next needs its own copy to advance and then return. In this case your choice is something like:

Widget next(Widget it, int n = 1){
    std::advance(it, n);
    return it;
}

vs

Widget next(Widget&& it, int n = 1){
    std::advance(it, n);
    return std::move(it);
}

I think by-value is better here. From the signature you can see it is taking a copy. If the caller wants to avoid a copy they can do a std::move and guarantee the variable is moved from but they can still pass lvalues if they want to. With pass-by-rvalue-reference the caller cannot guarantee that the variable has been moved from.

Move-assignment to a copy

Let's say you have a class WidgetHolder:

class WidgetHolder {
    Widget widget;
   //...
};

and you need to implement a setWidget member function. I'm going to assume you already have an overload that takes a reference-to-const:

WidgetHolder::setWidget(const Widget& w) {
    widget = w;
}

but after measuring performance you decide you need to optimize for r-values. You have a choice between replacing it with:

WidgetHolder::setWidget(Widget w) {
    widget = std::move(w);
}

Or overloading with:

WidgetHolder::setWidget(Widget&& widget) {
    widget = std::move(w);
}

This one is a little bit more tricky. It is tempting choose pass-by-value because it accepts both rvalues and lvalues so you don't need two overloads. However it is unconditionally taking a copy so you can't take advantage of any existing capacity in the member variable. The pass by reference-to-const and pass by r-value reference overloads use assignment without taking a copy which might be faster

Move-construct a copy

Now lets say you are writing the constructor for WidgetHolder and as before you have already implemented a constructor that takes an reference-to-const:

WidgetHolder::WidgetHolder(const Widget& w) : widget(w) {
}

and as before you have measured peformance and decided you need to optimize for rvalues. You have a choice between replacing it with:

WidgetHolder::WidgetHolder(Widget w) : widget(std::move(w)) {
}

Or overloading with:

WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) {
}

In this case, the member variable cannot have any existing capacity since this is the constructor. You are move-constucting a copy. Also, constructors often take many parameters so it can be quite a pain to write all the different permutations of overloads to optimize for r-value references. So in this case it is a good idea to use pass-by-value, especially if the constructor takes many such parameters.

Passing unique_ptr

With unique_ptr the efficiency concerns are less important given that a move is so cheap and it doesn't have any capacity. More important is expressiveness and correctness. There is a good discussion of how to pass unique_ptr here.

Solution 4

One issue not mentioned in the other answers is the idea of exception-safety.

In general, if the function throws an exception, we would ideally like to have the strong exception guarantee, meaning that the call has no effect other than raising the exception. If pass-by-value uses the move constructor, then such an effect is essentially unavoidable. So an rvalue-reference argument may be superior in some cases. (Of course, there are various cases where the strong exception guarantee isn't achievable either way, as well as various cases where the no-throw guarantee is available either way. So this is not relevant in 100% of cases. But it's relevant sometimes.)

Solution 5

When you pass by rvalue reference object lifetimes get complicated. If the callee does not move out of the argument, the destruction of the argument is delayed. I think this is interesting in two cases.

First, you have an RAII class

void fn(RAII &&);

RAII x{underlying_resource};
fn(std::move(x));
// later in the code
RAII y{underlying_resource};

When initializing y, the resource could still be held by x if fn doesn't move out of the rvalue reference. In the pass by value code, we know that x gets moved out of, and fn releases x. This is probably a case where you would want to pass by value, and the copy constructor would likely be deleted, so you wouldn't have to worry about accidental copies.

Second, if the argument is a large object and the function doesn't move out, the lifetime of the vectors data is larger than in the case of pass by value.

vector<B> fn1(vector<A> &&x);
vector<C> fn2(vector<B> &&x);

vector<A> va;  // large vector
vector<B> vb = fn1(std::move(va));
vector<C> vc = fn2(std::move(vb));

In the example above, if fn1 and fn2 don't move out of x, then you will end up with all of the data in all of the vectors still alive. If you instead pass by value, only the last vector's data will still be alive (assuming vectors move constructor clears the sources vector).

Share:
27,536
Mark
Author by

Mark

Updated on July 08, 2022

Comments

  • Mark
    Mark almost 2 years

    When should I declare my function as:

    void foo(Widget w);

    as opposed to

    void foo(Widget&& w);?

    Assume this is the only overload (as in, I pick one or the other, not both, and no other overloads). No templates involved. Assume that the function foo requires ownership of the Widget (e.g. const Widget& is not part of this discussion). I'm not interested in any answer outside the scope of these circumstances. See addendum at end of post for why these constraints are part of the question.

    The primary difference that my colleagues and I can come up with is that the rvalue reference parameter forces you to be explicit about copies. The caller is responsible for making an explicit copy and then passing it in with std::move when you want a copy. In the pass by value case, the cost of the copy is hidden:

        //If foo is a pass by value function, calling + making a copy:
        Widget x{};
        foo(x); //Implicit copy
        //Not shown: continues to use x locally
    
        //If foo is a pass by rvalue reference function, calling + making a copy:
        Widget x{};
        //foo(x); //This would be a compiler error
        auto copy = x; //Explicit copy
        foo(std::move(copy));
        //Not shown: continues to use x locally
    

    Other than that difference. Other than forcing people to be explicit about copying and changing how much syntactic sugar you get when calling the function, how else are these different? What do they say differently about the interface? Are they more or less efficient than one another?

    Other things that my colleagues and I have already thought of:

    • The rvalue reference parameter means that you may move the argument, but does not mandate it. It is possible that the argument you passed in at the call site will be in its original state afterwards. It's also possible the function would eat/change the argument without ever calling a move constructor but assume that because it was an rvalue reference, the caller relinquished control. Pass by value, if you move into it, you must assume that a move happened; there's no choice.
    • Assuming no elisions, a single move constructor call is eliminated with pass by rvalue.
    • The compiler has better opportunity to elide copies/moves with pass by value. Can anyone substantiate this claim? Preferably with a link to gcc.godbolt.org showing optimized generated code from gcc/clang rather than a line in the standard. My attempt at showing this was probably not able to successfully isolate the behavior: https://godbolt.org/g/4yomtt

    Addendum: why am I constraining this problem so much?

    • No overloads - if there were other overloads, this would devolve into a discussion of pass by value vs a set of overloads that include both const reference and rvalue reference, at which point the set of overloads is obviously more efficient and wins. This is well known, and therefore not interesting.
    • No templates - I'm not interested in how forwarding references fit into the picture. If you have a forwarding reference, you call std::forward anyway. The goal with a forwarding reference is to pass things as you received them. Copies aren't relevant because you just pass an lvalue instead. It's well known, and not interesting.
    • foo requires ownership of Widget (aka no const Widget&) - We're not talking about read-only functions. If the function was read-only or didn't need to own or extend the lifetime of the Widget, then the answer trivially becomes const Widget&, which again, is well known, and not interesting. I also refer you to why we don't want to talk about overloads.
  • Mark
    Mark almost 8 years
    This answer avoids the question. Aside from the first sentence, the answer is factually correct, but has no relevance. I reject the first sentence based on the enumerated list of differences in the OP.
  • Cheers and hth. - Alf
    Cheers and hth. - Alf almost 8 years
    @Mark: If I knew a way to stop you from this foolishness, I wouldn't.
  • yfeldblum
    yfeldblum almost 8 years
    To be clear, the question is not about the sensible choice between the two enumerated alternatives in the delimited circumstances given. That would be a bit of a silly question. The question is actually about the technical differences and the differences in the code-to-human communication between the two enumerated alternatives, with the delimited circumstances offered in order to isolate the case under consideration and to reduce it to its minimal form.
  • Mark
    Mark almost 8 years
    Your rvalue parameter version of next does more work than it needs to. Its interface says "I own the iterator you give me"; there's no need to make a copy inside the function. That happens at the call-site, if and only if it is necessary. The discussion about next is therefore moot. The rest of this discussion is entirely about overloading where const& is one of the overloads. In that case it's already well known that rvalue parameters are better. Not interested in those cases.
  • Mark
    Mark almost 8 years
    Correction to my last comment: no need to make a copy or a move inside the function. The caller must assume it has been moved unless otherwise hinted. Also lines of code != lines of assembly.
  • Chris Drew
    Chris Drew almost 8 years
    @Mark You are right that I don't need to explicitly make a copy in next although it is just deferring the copy until the return. Regarding "...it's already well known that rvalue parameters are better" I tried to provide an example (a constructor) where by-value is better.
  • Cheers and hth. - Alf
    Cheers and hth. - Alf almost 8 years
    Pass by rvalue-ref does not express the same interface as pass by value. The calling code can't do the same things. So when you recommend one over the other depending on the kind of type, it's not meaningful.
  • Mark
    Mark almost 8 years
    This answers misses the potential for someone to move a Widget or Car into the first case, thereby still avoiding a copy. The void foo(Car c) case doesn't say we each have a car, it says "I need ownership of a car", it might be a new one (a copy), it might be that you've decided you don't need yours anymore and gave it to me. At that point the difference between your case 1 and 4 is explicitness of copies (as far as the interface goes). Then, and only then, does the performance question become interesting. The other cases remain out of scope.
  • Altainia
    Altainia almost 8 years
    @Mark You are correct; moving a car into void foo(Car c) would result in the same thing, in the end; it's just a contrived way of doing it. Jane wants a car, Bob doesn't need his anymore. But Jane doesn't want to impose on Bob so she refuses to take Bob's personal car; only wants one like it. Bob, wanting to get rid of his car, then pretends to get her a new one but really gives her his. That use case is two people not really agreeing what the interface should be. Also, I mentioned the other cases for completeness :)
  • Chris Drew
    Chris Drew almost 8 years
    @Mark if you choose the constructor that takes the parameter by value then you don't need the pass by reference-to-const constructor anymore.
  • Mark
    Mark almost 8 years
    I agree, but the language you used to describe the situation above doesn't say that. It makes the comparison I showed in my last link.
  • Mark
    Mark almost 8 years
    This is a solid, well reasoned difference nobody else touched on. This is subtle and awesome. Can be addressed with template<typename T> typename std::remove_reference<T>::type move_fo_realzies(T&& x) { return std::move(x); }. The issue only comes into play if you use std::move instead of move_fo_realzies... but getting people to use the latter would require some additional re-education.
  • Jarod42
    Jarod42 almost 7 years
    @Cheersandhth.-Alf: In the context of the question, both express ownership transfer, one allows implicit copy.
  • Cheers and hth. - Alf
    Cheers and hth. - Alf almost 7 years
    @Jarod42: Evidently what I replied to back then has been removed in a cleanup. And now I'm a bit confused by my own arguments. I still think the best choice is to pass by rvalue reference, but I think that it is best because it communicates most clearly the ownership-taking, to a reader of the code. I can't see that I wrote anything about source code as communication, in the answer. Hm!
  • dragonxlwang
    dragonxlwang about 5 years
    In move-construct a copy, why WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) is not good? I think this should be considered better than pass-by-value since we are implementing the semantics of move-construct for widget holder: see example in en.cppreference.com/w/cpp/language/move_constructor
  • Chris Drew
    Chris Drew about 5 years
    @dragonxlwang Note this is not a move constructor for WidgetHolder That would be: WidgetHolder::WidgetHolder(WidgetHolder&&). This is a normal constructor that happens to take a Widget. There is nothing particularly wrong with passing by r-value reference in this case but you have to have two overloads WidgetHolder::WidgetHolder(Widget&& w) and WidgetHolder::WidgetHolder(const Widget& w) instead of one without any particular benefit. This is particularly bad if you have a constructor with many parameters because then you need many permutations of overloads.
  • Lapo
    Lapo about 4 years
    This is one of the best explainations about pass by value, lvalue ref, const lvalue ref and rvalue ref I've ever read. Really!
  • Siddu
    Siddu almost 3 years
    I small doubt about the overload setWidget(Widget&& widget), it seems the std::move() is unnecessary because rvalue reference should implicitly call the move assignment operator if it exists, am I missing something?
  • Chris Drew
    Chris Drew almost 3 years
    @Siddu an rvalue reference is itself an lvalue so if you don't do std::move it will try to do a copy assignment and not a move assignment.
  • FreePhoenix888
    FreePhoenix888 over 2 years
    @Altainia Your comments in the code are the best. You are not just a good programmer, you are a good teacher. Thank you :)