Pass by value vs pass by rvalue reference

前端 未结 7 946
独厮守ぢ
独厮守ぢ 2020-11-29 01:40

When should I declare my function as:

void foo(Widget w);

as opposed to

void foo(Widget&& w);?

Assume this

相关标签:
7条回答
  • 2020-11-29 01:44

    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).

    0 讨论(0)
  • 2020-11-29 01:44

    Choosing between by-value and by-rvalue-ref, with no other overloads, is not meaningful.

    With pass by value the actual argument can be an lvalue expression.

    With pass by rvalue-ref the actual argument must be an rvalue.


    If the function is storing a copy of the argument, then a sensible choice is between pass-by-value, and a set of overloads with pass-by-ref-to-const and pass-by-rvalue-ref. For an rvalue expression as actual argument the set of overloads can avoid one move. It's an engineering gut-feeling decision whether the micro-optimization is worth the added complexity and typing.

    0 讨论(0)
  • 2020-11-29 01:46

    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.

    0 讨论(0)
  • 2020-11-29 01:47

    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.)

    0 讨论(0)
  • 2020-11-29 01:53

    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.

    0 讨论(0)
  • 2020-11-29 02:01

    One notable difference is that if you move to an pass-by-value function:

    void foo(Widget w);
    foo(std::move(copy));
    

    compiler must generate a move-constructor call Widget(Widget&&) to create the value object. In case of pass-by-rvalue-reference no such call is needed as the rvalue-reference is passed directly to the method. Usually this does not matter, as move constructors are trivial (or default) and are inlined most of the time. (you can check it on gcc.godbolt.org -- in your example declare move constructor Widget(Widget&&); and it will show up in assembly)

    So my rule of thumb is this:

    • if the object represents a unique resource (without copy semantics) I prefer to use pass-by-rvalue-reference,
    • otherwise if it logically makes sense to either move or copy the object, I use pass-by-value.
    0 讨论(0)
提交回复
热议问题