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?
In C++20, the only copy allowed in the example is the one at line 3 (actually, x is implicitly moved from). Copy elision (NRVO) is allowed there and is routinely performed by most compilers, but is still non-guaranteed, and the widget class cannot be non-copyable non-movable.
Copy elision is an optimization implemented by most compilers to prevent extra (potentially expensive) copies in certain situations. It makes returning by value or pass-by-value feasible in practice (restrictions apply).
This will perform 1 copy construction and 3 move constructions.
a
to bind to lhs
.lhs
out of the first +
.+
will bind to the by value lhs
parameter of the second +
with elision.lhs
will incur the second move construction.lhs
will incur the third move construction.+
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With