This week-end I looked at C++ rvalue references and move semantics. Here’s the code, the good stuff is in matrix.h.
Rvalue references seem useful, but way too treacherous. Any programming construct that raises doubts on the nature of objective reality and evokes associations with quantum physics (‘reference collapse’ ~ ‘wave function collapse’) is probably too complicated for its own good. But… maybe you don’t believe me? The great Scott Meyers explains it beautifully in his channel 9 presentation on rvalue references. I was unable to get it right on my own before I watched that.
Rvalue references can be used to reduce unnecessary data copying. I was able to create a matrix class that passes matrices by value, but avoids actually duplicating the data whenever possible. This is great, but let’s see the laundry list of gotchas I ran into:
- Variable of type
T&&
cannot be passed to a function that acceptsT&&
(oops!). - You fix it by using
forward(v)
ormove(v)
, but the difference between the two is confusing. - When you see
T&&
in code, it may not in fact be an rvalue reference. - If you are not careful, you may silently get copy behavior instead of move behavior.
- Overloads may not work the way you expect.
But let’s start with the good stuff.
What you can do with rvalue references.
I created a class matrix
with operator+
: complete source at https://github.com/ikriv/cpp-move-semantics.
Rvalue references are traditionally used to optimizing copy constructors and assignment, but they can be as useful in case of operator +. Traditional implementation of such operator for matrices would look something like this:
matrix operator+(matrix const& a, matrix const& b) { matrix result = a; result += b; return result; }
If we have an experssion like matrix result = a+f(b)+f(c)+d;
the compiler will have no choice, but to create a bunch of temporary matrices and copy a lot of data around. We can significantly reduce the amount of copying by adding rvalue-based overloads of operator +() that modify an rvalue parameter “in place” instead of allocating a new matrix. The idea is that rvalue is by definition inaccessible outside the expression, and thus there is no harm in modifying it.
matrix operator+(matrix&& a, matrix const& b) // rvalue+lvalue { return std::move(a += b); } matrix operator+(matrix const& a, matrix&& b) // lvalue+rvalue { return std::move(b += a); } matrix operator+(matrix&& left, matrix&& right) // rvalue+rvalue { return std::move(a += b); }
This does improve the performance dramatically. With the move semantics my code allocates 8 matrices. All rvalue reference overloads are conditionally compiled via #define MOVE_SEMANTICS. If I turn that off, the number of matrices allocated jumps to 12 in release mode and to 24 in debug mode.
So, rvalue references are useful, but they are still a minefield. You must really want those optimizations, and even then if you are not careful, you may not get what you expected. To be continued…
Permalink
Permalink