打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Rvalue Reference Quick Look
Document number: N2027=06-0097

Howard E. Hinnant
Bjarne Stroustrup
Bronek Kozicki
2006-06-12

A Brief Introduction to Rvalue References

Abstract

Rvalue references is a small technical extension to the C++ language.Rvalue references allow programmers to avoid logically unnecessary copying andto provide perfect forwarding functions. They are primarily meant to aid in thedesign of higer performance and more robust libraries.

Contents

Introduction

This document gives a quick tour of the new C++ language featurervalue reference. It is a brief tutorial, rather than a complete reference.For details, see these references.

The rvalue reference

An rvalue reference is a compound type very similar to C++'s traditional reference.To better distinguish these two types, we refer to a traditional C++ referenceas an lvalue reference. When the term reference is used, it refers toboth kinds of reference: lvalue reference and rvalue reference.

An lvalue reference is formed by placing an & after some type.

A a;A& a_ref1 = a;  // an lvalue reference

An rvalue reference is formed by placing an && after some type.

A a;A&& a_ref2 = a;  // an rvalue reference

An rvalue reference behaves just like an lvalue reference except that it can bindto a temporary (an rvalue), whereas you can not bind a (non const) lvalue reference to anrvalue.

A&  a_ref3 = A();  // Error!A&& a_ref4 = A();  // Ok

Question: Why on Earth would we want to do this?!

It turns out that the combination of rvalue references and lvalue references isjust what is needed to easily code move semantics. The rvalue reference canalso be used to achieve perfect forwarding, a heretofore unsolved problem inC++. From a casual programmer's perspective, what we get from rvalue references is more general andbetter performing libraries.

Move Semantics

Eliminating spurious copies

Copying can be expensive.For example, for std::vectors, v2=v1 typically involves a function call,a memory allocation, and a loop. This is of course acceptable where we actually need twocopies of a vector, but in many cases, we don't:We often copy a vector from one place to another, just to proceed to overwrite the old copy.Consider:
template <class T> swap(T& a, T& b){    T tmp(a);   // now we have two copies of a    a = b;      // now we have two copies of b    b = tmp;    // now we have two copies of tmp (aka a)}
But, we didn't want to have any copies of a or b,we just wanted to swap them.Let's try again:
template <class T> swap(T& a, T& b){    T tmp(std::move(a));    a = std::move(b);       b = std::move(tmp);}

This move() gives its target the value of its argument, but is not obliged topreserve the value of its source. So, for a vector, move() couldreasonably be expected to leave its argument as a zero-capacity vector to avoid having tocopy all the elements. In other words, move is a potentially destructive read.

In this particular case, we could have optimized swap by a specialization.However, we can't specialize every function that copies a large object just before itdeletes or overwrites it. That would be unmanageable.

The first task of rvalue references is to allow us to implement move() withoutverbosity, or rutime overhead.

move

The move function really does very little work. Allmove does is accept either an lvalue or rvalue argument, and return itas an rvalue without triggering a copy construction:

template <class T>typename remove_reference<T>::type&&move(T&& a){    return a;}

It is now up to client code to overload key functions on whether their argument isan lvalue or rvalue (e.g. copy constructor and assignment operator). When the argumentis an lvalue, the argument must be copied from. When it is an rvalue, it can safelybe moved from.

Overloading on lvalue / rvalue

Consider a simple handle class that owns a resource and also providescopy semantics (copy constructor and assignment). For example a clone_ptrmight own a pointer, and call clone() on it for copying purposes:

template <class T>class clone_ptr{private:    T* ptr;public:    // construction    explicit clone_ptr(T* p = 0) : ptr(p) {}    // destruction    ~clone_ptr() {delete ptr;}    // copy semantics    clone_ptr(const clone_ptr& p)        : ptr(p.ptr ? p.ptr->clone() : 0) {}    clone_ptr& operator=(const clone_ptr& p)    {        if (this != &p)        {            delete ptr;            ptr = p.ptr ? p.ptr->clone() : 0;        }        return *this;    }    // move semantics    clone_ptr(clone_ptr&& p)        : ptr(p.ptr) {p.ptr = 0;}    clone_ptr& operator=(clone_ptr&& p)    {        std::swap(ptr, p.ptr);        return *this;    }    // Other operations    T& operator*() const {return *ptr;}    // ...};

Except for the highlighted move semantics section above, clone_ptris code that you might find in today's books on C++. Clients of clone_ptrmight use it like so:

clone_ptr<base> p1(new derived);// ...clone_ptr<base> p2 = p1;  // p2 and p1 each own their own pointer

Note that copy constructing or assigning a clone_ptr is a relatively expensive operation.However when the source of the copy is known to be an rvalue, one can avoid the potentiallyexpensive clone() operation by pilfering the rvalue's pointer (no one will notice!).The move constructor above does exactly that, leaving the rvalue in a default constructedstate. The move assignment operator simply swaps state with the rvalue.

Now when code tries to copy an rvalue clone_ptr, or if that code explicitly givespermission to consider the source of the copy an rvalue (using std::move), theoperation will execute much faster.

clone_ptr<base> p1(new derived);// ...clone_ptr<base> p2 = std::move(p1);  // p2 now owns the pointer intead of p1

For classes made up of other classes (via either containment or inheritance), themove constructor and move assignment can easily be coded using the std::movefunction:

class Derived    : public Base{    std::vector<int> vec;    std::string name;    // ...public:    // ...    // move semantics    Derived(Derived&& x)              // rvalues bind here        : Base(std::move(x)),           vec(std::move(x.vec)),          name(std::move(x.name)) { }    Derived& operator=(Derived&& x)   // rvalues bind here    {        Base::operator=(std::move(x));        vec  = std::move(x.vec);        name = std::move(x.name);        return *this;    }    // ...};

Each subobject will now be treated as an rvalue when binding to the subobject's constructors and assignment operators.std::vector and std::string have move operations coded (just likeour eariler clone_ptr example) which will completely avoid the tremendouslymore expensive copy operations.

Note above that the argument x is treatedas an lvalue internal to the move functions, even though it is declared as an rvaluereference parameter. That's why it is necessary to say move(x) instead ofjust x when passing down to the base class. This is a key safety featureof move semantics designed to prevent accidently moving twice from some named variable.All moves occur only from rvalues, or with an explicit cast to rvalue such as usingstd::move. If you have a name for the variable, it is an lvalue.

Question: What about types that don't own resources? (E.g. std::complex?)

No workneeds to be done in that case. The copy constructor is already optimal when copyingfrom rvalues.

Movable but Non-Copyable Types

Some types are not amenable to copy semantics but can still be made movable. For example:

  • fstream
  • unique_ptr (non-shared, non-copyable ownership)
  • A type representing a thread of execution

By making such types movable (though still non-copyable) their utility is tremendouslyincreased. Movable but non-copyable types can be returned by value from factory functions:

ifstream find_and_open_data_file(/* ... */);...ifstream data_file = find_and_open_data_file(/* ... */);  // No copies!

In the above example, the underlying file handle is passed from object to object, as longas the source ifstream is an rvalue. At all times, there is still only one underlyingfile handle, and only one ifstream owns it at a time.

Movable but non-copyable types can also safely be put into standard containers. If the containerneeds to "copy" an element internally (e.g. vector reallocation) it will move theelement instead of copy it.

vector<unique_ptr<base>> v1, v2;v1.push_back(unique_ptr<base>(new derived()));  // ok, moving, not copying...v2 = v1;             // Compile time error.  This is not a copyable type.v2 = move(v1);       // Move ok.  Ownership of pointers transferred to v2.

Many standard algorithms benefit from moving elements of the sequence asopposed to copying them. This not only provides betterperformance (like the improved std::swap implementation described above),but also allows these algorithms to operate on movable but non-copyable types. Forexample the following code sorts a vector<unique_ptr<T>> basedon comparing the pointed-to types:

struct indirect_less{    template <class T>    bool operator()(const T& x, const T& y)        {return *x < *y;}};...std::vector<std::unique_ptr<A>> v;...std::sort(v.begin(), v.end(), indirect_less());

As sort moves the unique_ptr's around, it will useswap (which no longer requires Copyability) or moveconstruction / move assignment. Thus during the entire algorithm, theinvariant that each item is owned and referenced by one and only one smart pointeris maintained. If the algorithm were to attempt a copy (say by programmingmistake) a compile time error would result.

Perfect Forwarding

Consider writing a generic factory function that returns a std::shared_ptr for a newly constructedgeneric type. Factory functions such asthis are valuable for encapsulating and localizing the allocation of resources.Obviously, the factory function must accept exactly the same sets of arguments as the constructors of the type of objects constructed.Today this might be coded as:

template <class T>std::shared_ptr<T>factory()   // no argument version{    return std::shared_ptr<T>(new T);}template <class T, class A1>std::shared_ptr<T>factory(const A1& a1)   // one argument version{    return std::shared_ptr<T>(new T(a1));}// all the other versions

In the interest of brevity, we will focus on just the one-parameter version. For example:

std::shared_ptr<A> p = factory<A>(5);

Question: What if T's constructor takes a parameter by non-const reference?

In that case, we get a compile-time error as theconst-qualifed argument of the factory function will not bind to the non-constparameter of T's constructor.

To solve that problem, we could use non-const parameters in our factory functions:

template <class T, class A1>std::shared_ptr<T>factory(A1& a1){    return std::shared_ptr<T>(new T(a1));}

This is much better. If a const-qualified type is passed to the factory, theconst will be deduced into the template parameter (A1 for example) and thenproperly forwarded to T's constructor. Similarly, if a non-const argument is given tofactory, it will be correctly forwarded to T's constructor as a non-const.Indeed, this is precisely how forwarding applications are coded today (e.g. std::bind).

However, consider:

std::shared_ptr<A> p = factory<A>(5);	// errorA* q = new A(5);	                // ok

This example worked with our first version of factory, but now it's broken:The "5" causes the factory template argument to be deduced as int&and subsequently will not bind to the rvalue "5". Neither solution so far is right.Each breaks reasonable and common code.

Question: What about overloading on every combination of AI& andconst AI&?

This would allow us to handle all examples, but at a cost of an exponential explosion:For our two-parameter case, this would require 4 overloads.For a three-parameter factory we would need 8 additional overloads. For a four-parameter factory we would need 16, and so on. This is not a scalable solution.

Rvalue references offer a simple, scalable solution to this problem:

template <class T, class A1>std::shared_ptr<T>factory(A1&& a1){    return std::shared_ptr<T>(new T(std::forward<A1>(a1)));}

Now rvalue arguments can bind to the factory parameters.If the argument is const,that fact gets deduced into the factory template parameter type.

Question: What is that forward function in our solution?

Like move, forward is a simple standard library function used toexpress our intent directly and explicitly, rather than through potentially crypticuses of references. We want to forward the argument a1, so we simply say so.

Here, forward preserves the lvalue/rvalue-ness of the argument that was passed tofactory. If an rvalue is passed to factory, then anrvalue will be passed to T's constructor with the help of the forwardfunction. Similarly, if an lvalue is passed to factory, it is forwarded toT's constructor as an lvalue.

The definition of forward looks like this:

template <class T>struct identity{    typedef T type;};template <class T>T&& forward(typename identity<T>::type&& a){    return a;}

References

As one of the main goals of this paper is brevity, there are details missing from the abovedescription. But the above content represents 95% of the knowledge with a fraction of thereading.

For further details on the motivation of move semantics, such as performance tests,details of movable but non-copyable types, and many other details please seeN1377.

For a very thorough treatment of the forwarding problem, please seeN1385.

For further applications of the rvalue reference (besides move semanticsand perfect forwarding), please seeN1690.

For proposed wording for the language changes required to standardize thervalue reference, please seeN1952.

For a summary of the impact the rvalue reference will have on the standardlibrary, please seeN1771.

For proposed wording for the library changes required to take advantage of thervalue reference, please see:

For a proposal to extend the rvalue reference to the implicit object parameter (this),please seeN1821.

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
c++
C++11新特性学习笔记
深入右值引用,move语义和完美转发
Lvalues, rvalues and references | Andrzej's C++ blog
详解C++右值引用
c++中的左值与右值
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服