Making Your Next Move

来源:互联网 发布:java 获取当月第一天 编辑:程序博客网 时间:2024/05/16 19:36
This entry is part of a series, RValue References: Moving Forward»Entries in this series:
  1. Want Speed? Pass by Value.
  2. Making Your Next Move
  3. Your Next Assignment...
  4. Exceptionally Moving!
  5. Onward, Forward!
Powered by Hackadelic Sliding Notes 1.6.5

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 lvalue

I realize that’s counterintuitive, but consider the following:

int g(X const&);  // logically non-mutatingint g(X&&);       // ditto, but moves from rvalues int f(X&& a){    g(a);    g(a);}

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:

#include <utility> // for std::moveint f(X&& a){    g(a);    g( std::move(a) );}

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:

class Matrix{     …     std::vector<double> storage;}; Matrix operator+(Matrix const& left, Matrix const& right){    Matrix result(left);    result += right;   // delegates to +=    return result;}Matrix a, b, c, d;…Matrix x = a + b + c + d;

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:

// Guess that the first argument is more likely to be an rvalueMatrix operator+(Matrix x, Matrix const& y){    x += y;        // x was passed by value, so steal its vector    Matrix temp;   // Compiler cannot RVO x, so    swap(x, temp); // make a new Matrix and swap    return temp;}Matrix x = a + b + c + d;

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:

Matrix x = a + (b + (c + d));

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:

// The "usual implementation"Matrix operator+(Matrix const& x, Matrix const& y){ Matrix temp = x; temp += y; return temp; } // --- Handle rvalues --- Matrix operator+(Matrix&& temp, const Matrix& y){ temp += y; return std::move(temp); } Matrix operator+(const Matrix& x, Matrix&& temp){ temp += x; return std::move(temp); } Matrix operator+(Matrix&& temp, Matrix&& y){ temp += y; return std::move(temp); }

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_ptrs 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:

template <class T>struct unique_ptr{ private:    unique_ptr(const unique_ptr& p);    unique_ptr& operator=(const unique_ptr& p); public:    unique_ptr(unique_ptr&& p)      : ptr_(p.ptr_) { p.ptr_ = 0; }     unique_ptr& operator=(unique_ptr&& p)    {        delete ptr_; ptr_ = p.ptr_;        p.ptr_ = 0;        return *this;    }private:     T* ptr_;};

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:

int f(std::unique_ptr<T>);    // accepts a move-only type by valueunique_ptr<T> x;              // has a name so it's an lvalueint a = f( x );               // error! (requires a copy of x)int b = f( std::move(x) );    // OK, explicit 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.


  1. 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. ↩

Value questions/comments:
============================
Howard Hinnant
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.

Dave Abrahams
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.

From
http://cpp-next.com/archive/2009/09/making-your-next-move/#comment-153
0 0
原创粉丝点击