Making Your Next Move

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-mutating
int 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::move
int 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 rvalue
Matrix 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 value
unique_ptr<T> x;              // has a name so it's an lvalue
int 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.

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值