-2

I've been reading about rvalue references and std::move, and I have a performance question. For the following code:

template<typename T>
void DoSomething()
{
  T x = foo();
  bar(std::move(x));
}

Would it be better, from a performance perspective, to rewrite this using an rvalue reference, like so?

template<typename T>
void DoSomething()
{
  T&& x = foo();
  bar(x);
}

If I understand rvalue references correctly, this will simply act as if bar(foo()) had been called, as pointed out by commenters. But the intermediate value may be needed; is it useful to do this?

George Hilliard
  • 15,402
  • 9
  • 58
  • 96

2 Answers2

4

The best way to use rvalue references / move semantics, is for the most part, don't try too hard. Return function locals by value. And construct them by value from such functions:

X foo();

template<typename T>
void DoSomething()
{
  T x = foo();
  ...

If you are the author of class X, then you probably want to ensure that X has efficient move construction and move assignment. If the copy constructor and copy assignment of X are already efficient, there is literally nothing more to do:

class X
{
     int data_;
public:
     // copy members just copy data_
};

If you find yourself allocating memory in X's copy constructor, and or copy assignment, then you should consider writing a move constructor and move assignment operator. Do your best to make them noexcept.

Although there are always exceptions to the rule, in general:

  1. Don't return by reference, either lvalue or rvalue reference, unless you want the client to have direct access to a non-function-local variable.

  2. Don't catch a return by reference, lvalue or rvalue. Yes, there are cases where it might save you something, without getting you into trouble. Don't even think about doing it as a rule. Only do something like this if your performance testing is highly motivating you to do so.

  3. All of the things you (hopefully) learned not to do with lvalue references are still just as dangerous to do with rvalue references (such as returning a dangling reference from a function).

  4. First program for correctness. This includes writing extensive and easy-to-run unit tests covering common and corner cases of your API. Then start optimizing. When you start out trying to write extremely optimized code (e.g. catching a return by reference) before you have a correct and well-tested solution, one inevitably writes code that is so brittle that the first bug fix that comes along just breaks the code further, but often in very subtle ways. This is a lesson that even experienced programmers (including myself) have to learn over and over.

  5. In spite of what I say in (4), even on the first write, keep an eye on O(N) performance. I.e. if your first try is so grossly slow due to basic overall design deficiencies or poor algorithms that it can't perform its basic function in a reasonable time, then you need more than new code, you need a new design. In this bullet I am not talking about things like whether or not you've caught a return with an rvalue reference. I'm talking about whether or not you've created an O(N^2) algorithm, when O(N log N), or O(N) could've done the job. This is all too easy to do when the job that needs to get done is non-trivial, or when the code is so complicated that you can't tell what is going on.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
1

Here std::move does not help.
As you have made a copy of the object (though elision may help).

template<typename T>
void DoSomething()
{
  T x = foo();
  bar(std::move(x));
}

Here rvalue reference is not helping
As the variable x is a named object (and thus not an rvalue reference (anymore)).

template<typename T>
void DoSomething()
{
  T&& x = foo();
  bar(x);
}

Best to use:

template<typename T>
void DoSomething()
{
  bar(foo());
}

But if you must use it locally:

template<typename T>
void DoSomething()
{
  T&& x = foo();
  //  Do stuff with x
  bar(std::move(x));
}

Though I am not 100% sure about the above and would love some feedback.

Martin York
  • 257,169
  • 86
  • 333
  • 562
  • 1
    Let's assume `foo()` yields a prvalue (i.e. not a reference), e.g. `MyClass foo();`. Then, in the first example: `T x = foo();` does nothing if possible (copy/move elision), moves if possible, or else copies (e.g. if there's a user-provided copy-, but no move-ctor). `bar(std::move(x))` may replace a copy by a move if `bar` takes by-value. `T&& x = foo();` might prevent a copy or move, although that's unlikely (copy/move from return value temporary, can be elided). It also works if copy/move is not allowed. – dyp Oct 19 '13 at 00:44
  • @DyP: So the first version will move if possible. This the `std::move` to call bar actually is useful. – Martin York Oct 19 '13 at 01:10
  • Yes, if `bar` takes by value and `T` is movable, the `std::move(x)` is useful in the first example (also, if `bar` has an rvalue-ref overload, it might alter semantics). Similarly, it *could* help if you added it in the second example (under the same conditions and for the same reasons as in the first example). – dyp Oct 19 '13 at 01:15
  • @DyP: OK. Agree on the first example. – Martin York Oct 19 '13 at 05:45
  • @DyP: Are you sure about the second example. Because `x` is a named variable it is no longer an rvalue reference. (this is non formative note) > n3690 Section `5 Expressions` [expr] Paragraph 7 In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue references to objects are treated as xvalues; rvalue references to functions are treated as lvalues whether named or not.. It is a note so non normative. But by my reading because it is a named variable collapses to an lvalue (not an rvalue). – Martin York Oct 19 '13 at 05:54
  • @LokiAstari You mixed up two different concepts: An expression has a type and belongs to a value category. The type of `x` is unchanged (still rvalue reference), but the value category of the _id-expression_ `x` is **lvalue**. – MWid Oct 19 '13 at 12:30
  • @LokiAstari Indeed, as MWid put it, the expression `x` in the second example in `bar(x)` yields an lvalue. What I meant was, you can write `bar(std::move(x))` in the second example as well: if `bar` takes by value, `bar(x)` copies, whereas `bar(std::move(x))` moves (if possible). OTOH, `T&& x = foo();` is similar to `T const& x = foo();`; both allow binding a reference to a temporary and are semantically different from `T x = foo();`. – dyp Oct 19 '13 at 13:04
  • @DyP: Thanks for all the clarification. – Martin York Oct 20 '13 at 00:42