18

Let's say I have the following type:

struct X {
    X& operator+=(X const&);
    friend X operator+(X lhs, X const& rhs) {
        lhs += rhs;
        return lhs;
    }
};

And I have the declaration (assume all the named variables are lvalues of type X):

X sum = a + b + c + d;

In C++17, what are the guarantees I have about how many copies and moves this expression will perform? What about non-guaranteed elision?

R Sahu
  • 204,454
  • 14
  • 159
  • 270
Barry
  • 286,269
  • 29
  • 621
  • 977

2 Answers2

19

This will perform 1 copy construction and 3 move constructions.

  1. Make a copy of a to bind to lhs.
  2. Move construct lhs out of the first +.
  3. The return of the first + will bind to the by value lhs parameter of the second + with elision.
  4. The return of the second lhs will incur the second move construction.
  5. The return of the third lhs will incur the third move construction.
  6. The temporary returned from the third + will be constructed at sum.

For each of the move constructions described above, there is another move construction that is optionally elided. So you are only guaranteed to have 1 copy and 6 moves. But in practice, unless you -fno-elide-constructors, you will have 1 copy and 3 moves.

If you don't reference a after this expression, you could further optimize with:

X sum = std::move(a) + b + c + d;

resulting in 0 copies and 4 moves (7 moves with -fno-elide-constructors).

The above results have been confirmed with an X which has instrumented copy and move constructors.


Update

If you're interested in different ways to optimize this, you could start with overload the lhs on X const& and X&&:

friend X operator+(X&& lhs, X const& rhs) {
    lhs += rhs;
    return std::move(lhs);
}
friend X operator+(X const& lhs, X const& rhs) {
    auto temp = lhs;
    temp += rhs;
    return temp;
}

This gets things down to 1 copy and 2 moves. If you are willing to restrict your clients from ever catching the return of your + by reference, then you can return X&& from one of the overloads like this:

friend X&& operator+(X&& lhs, X const& rhs) {
    lhs += rhs;
    return std::move(lhs);
}
friend X operator+(X const& lhs, X const& rhs) {
    auto temp = lhs;
    temp += rhs;
    return temp;
}

Getting you down to 1 copy and 1 move. Note that in this latest design, if you client ever does this:

X&& x = a + b + c;

then x is a dangling reference (which is why std::string does not do this).

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • So no chaining of elision? e.g. constructing the return of `a+b` directly into `operator+(??, c)`? – Barry Feb 10 '17 at 18:29
  • 2
    I don't even think it is legal to elide the return of `lhs`. However I also don't think any of the compiler writers have had any motivation to make it legal (no one attempts it anyway). – Howard Hinnant Feb 10 '17 at 18:31
  • Why it may be illegal? Can it became legal in the future? – Tomilov Anatoliy Feb 11 '17 at 07:21
  • @Orient: What I was trying to say is that the standard does not allow it. No one to my knowledge has ever proposed it. And I suspect the reason for that is that no one has figured out how to implement it. But those are guesses, I am not a compiler expert. – Howard Hinnant Feb 11 '17 at 15:22
9

OK, let's start with this:

X operator+(X lhs, X const& rhs) {
    lhs += rhs;
    return lhs;
}

This will always provoke a copy/move from the parameter to the return value object. C++17 doesn't change this, and no form of elision can avoid this copy.

Now, let's look at one part of your expression: a + b. Since the first parameter of operator+ is taken by value, a must be copied into it. So that's one copy. The return value will be copied out into the return prvalue. So that's 1 copy and one move/copy.

Now, the next part: (a + b) + c.

C++17 means that the prvalue returned from a + b will be used to directly initialize the parameter of operator+. This requires no copying/moving. But the return value from this will be copied from that parameter. So that's 1 copy and 2 moves/copies.

Repeat this for the last expression, and that's 1 copy and 3 move/copies. sum will be initialized from the prvalue expression, so no copying needs to be done there.


Your question really seems to be whether parameters remain excluded from elision in C++17. Because they were already excluded in prior versions. And that's not going to change; the reasons for excluding parameters from elision have not been invalidated.

"Guaranteed elision" only applies to prvalues. If it has a name, it cannot be a prvalue.

Community
  • 1
  • 1
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Some of these are going to be moves though - you mean 1 copy and 3 moves? – Barry Feb 10 '17 at 18:26
  • 1
    To put Nicol's comment about the parameter copy differently: taking an argument by value only helps for copy elision on the way in and *only* makes sense when *consuming* the parameter, i.e., `std::move()`ing it elsewhere, e.g.: `X rc(std::move(lhs)); rc += rhs; return rc;` – Dietmar Kühl Feb 10 '17 at 18:26
  • @Barry: Yes, I forgot that moves from returning local variables are automatic. – Nicol Bolas Feb 10 '17 at 18:48
  • 1
    I guess it'd've been cool if this whole thing could compile into the equivalent of `X sum(a); sum += b; sum += c; sum += d;` Guess that's still pretty hard. – Barry Feb 10 '17 at 19:08