- Want Speed? Pass by Value.
- Making Your Next Move
- Your Next Assignment...
- Exceptionally Moving!
- Onward, Forward!
This is the third article in a series about efficient value types in C++. In the previous installment, we introduced C++0x rvalue references, described how to build a movable type, and showed how to explicitly take advantage of that movability. Now we’ll look at another opportunity for move optimization and explore some new areas of the move landscape.
Resurrecting an Rvalue
Before we can discuss our next optimization, you need to know that an unnamed rvalue reference is an rvalue, but a named rvalue reference is an lvalue. I’ll write that again so you can let it sink in:
Important
A named rvalue reference is an lvalueI realize that’s counterintuitive, but consider the following:
if a
were treated as an rvalue inside f
, the first call to g
would move from a
, and the second would see a modifieda
. That is not just counter-intuitive; it violates the guarantee that calling g doesn’t visibly modify anything. So a named rvalue reference is just like any other reference, and only unnamed rvalue references are treated specially. To give the second call to g
a chance to move, we’d have to rewrite f
as follows:
Recall that std::move
doesn’t itself do any moving. It merely converts its argument into an unnamed rvalue reference so that move optimizations can kick in.
Binary Operators
Move semantics can be especially great for optimizing the use of binary operators. Consider the following code:
The Matrix
copy constructor gets invoked every time operator+
is called, to create result
. Therefore, even if RVO elides the copy of result
when it is returned, the expression above makes three Matrix
copies (one for each +
in the expression), each of which constructs a large vector. Copy elision allows one of these result
matrices to be the same object as x
, but the other two will need to be destroyed, which adds further expense.
Now, it is possible to write operator+
so that it does better on our expression, even in C++03:
A compiler that elides copies wherever possible will do a near-optimal job with that implementation, making only one temporary and moving its contents directly into x
. However, aside from being ugly, it’s easy to foil our optimization:
This is actually worse than we’d have done with a naive implementation: now the rvalues always appear on the right-hand side of the +
operator, and are copied explicitly. Lvalues always appear on the left-hand side, but are passed by value, and thus are copied implicitly with no hope of elision, so we make six expensive copies.
With rvalue references, though, we can do a reliably optimal1 job by adding overloads to the original implementation:
Move-Only Types
Some types really shouldn’t be copied, but passing them by value, returning them from functions, and storing them in containers makes perfect sense. One example you might be familiar with is std::auto_ptr<T>
: you can invoke its copy constructor, but that doesn’t produce a copy. Instead… it moves! Now, moving from an lvalue with copy syntax is even worse for equational reasoning than reference semantics is. What would it mean to sort a container of auto_ptr
s if copying a value out of the container altered the original sequence?
Because of these issues, the original standard explicitly outlawed the use of auto_ptr
in standard containers, and it has been deprecated in C++0x. Instead, we have a new type of smart pointer that can’t be copied, but can still move:
A unique_ptr
can be placed in a standard container and can do all the things auto_ptr
can do, except implicitly move from an lvalue. If you want to move from an lvalue, you simply pass it through std::move
:
Other types that will be move-only in C++0x include stream types, threads and locks (from new mulithreading support), and any standard container holding move-only types.
C++Next Up
There’s still lots to cover. Among other topics in this series, we’ll touch on exception safety, move assignment (again), perfect forwarding, and how to move in C++03. Stay tuned!
Please follow this link to the next installment.
-
Technically, you can do still better with expression templates, by delaying evaluation of the whole expression until assignment and adding all the matrices “in parallel,” making only one pass over the result. It would be interesting to know if there is a problem that has a truly optimal solution with rvalue references; one that can’t be improved upon by expression templates. ↩
============================
Why can’t the compiler automatically add std::move to the last use of an lvalue?
Mainly because this would have been dangerous before a very recent change we had to make to the langauge, and since then, no one has implemented, gained experience with, and proposed this change. It takes a lot of time and work to push something through the standardization process.
When and why use std::move in a return statement?
Use std::move when the argument is not eligible for RVO, and you do want to move from it.
Is a=std::move(a); legal?
It is advisable to allow self-move assignment in only very limited circumstances. Namely when “a” has already been moved from (is resourceless). It is my hope that the move assignment operator need not go to the trouble or expense of checking for self move assignment:
http://home.roadrunner.com/~hinnant/issue_review/lwg-active.html#1204
I.e. A::operator(A&& a) should be able to assume that “a” really does refer to a temporary.
Marc: Thanks for continuing with these articles. Reading this causes a lot of “why?”.
You’re welcome. I’ve been hoping people would ask some of these questions. While it sounds like you may have the answers all figured out, I’ll write my answers for everyone else’s benefit.
Why can’t the compiler automatically add std::move to the last use of an lvalue?
In general, the answer is that it could break existing code—see the scopeguard example in this posting by Niklas Matthies. If I was considering the design of a new language focused on value semantics (let’s call it V++), I might consider loosening those rules and using an explicit construct for scopeguard-ish things instead, but I would want to be sure not to break cases like this one:
struct X { … }; struct Y { Y(X& x) : x_(x) {} ~Y() { do_something_with(x_); } X& x_; }; void f() { X a; Y b(a); … }
No matter what else happens, Y::~Y()
should not encounter an x_
that has been implicitly moved from.
When and why use std::move in a return statement?
When you need to move from an lvalue that doesn’t name a local value with automatic storage duration.
The near optimal version for Matrix with a swap looks highly artificial, why can’t the RVO be cleverer?
The need for the swap is explained here.
Is a=std::move(a); legal?
It’s legal, but not a good idea. Unlike with copy assignment, it’s fairly hard to manufacture a case where a self-move-assignment occurs by mistake, and move assignment is often so fast already that an extra test-and-branch to handle self-assignment can account for a significant fraction of its overall cost, so it’s probably a better practice not to do it. I’ll have more to say about this in the series’ next article.
http://cpp-next.com/archive/2009/09/making-your-next-move/#comment-153