右值引用简介(中英文对照)

[译注:原文出处:http://www.artima.com/cppsource/rvalue.html]

[译注:本文介绍的“右值引用”是C++11的新特性]

A Brief Introduction to Rvalue References

by Howard E. Hinnant, Bjarne Stroustrup, and Bronek Kozicki

March 10, 2008

右值引用简介

Howard E. Hinnant、Bjarne Stroustrup和Bronek Kozicki

2008年3月10日

 

Summary

摘要

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

右值引用是C++语言的一处微小的技术扩展。右值引用使程序员可以避免逻辑上不必要的拷贝并提供完美的转发函数。其主要目的是协助更高性能和更加健壮的库的设计。

Introduction

引言

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

本文简洁明快地介绍了C++语言的新特性:右值引用。这只是一份简短的教程,而非完整的参考。更多细节可以看看参考文献。

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++ reference as anlvalue reference. When the term reference is used, it refers to both kinds of reference: lvalue reference and rvalue reference.

右值引用是一种复合类型,跟C++的传统引用很类似。为更准确地区分两种类型,我们把传统的C++引用称为左值引用。而使用“引用”这一术语时,我们的意思同时包含两种引用:左值引用和右值引用。

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 bind to a temporary (an rvalue), whereas you can not bind a (non const) lvalue reference to an rvalue.

右值引用的行为跟左值引用类似,不同之处在于:右值引用可以绑定到临时量(右值),而(非const的)左值引用却不能绑定到右值。

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 is just what is needed to easily codemove semantics. The rvalue reference can also be used to achieve perfect forwarding, a heretofore unsolved problem in C++. From a casual programmer's perspective, what we get from rvalue references is more general and better performing libraries.

事实证明,右值引用和左值引用结合起来恰能方便地实现转移语义。右值引用还可以用于实现完美转发,这是C++里面到现在都没有解决的一个问题。从一般程序员的角度来看,使用右值引用,我们得到的将是更加通用,性能也更高的库。

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 two copies 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:

拷贝的开销可以很大。举例来说,对于std::vector,像v2=v1这样的赋值通常包含一次函数调用,一次内存分配和一个循环。当我们确实需要一个vector的两份拷贝时,这当然是可接受的,然而很多情况下我们并不需要:我们常常将一个vector从一个地方复制到另一个地方,接着便覆写了旧的版本。考虑下面的代码:

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:

但我们并不想拥有a或b的任何拷贝,而只是想交换他们。再试试其它办法:

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 to preserve the value of its source. So, for a vector, move() could reasonably be expected to leave its argument as a zero-capacity vector to avoid having to copy all the elements. In other words, move is a potentially destructive read.

move()将其参数的值交给目标变量,但并不保证维持源变量的值。因此对vector来说,我们完全可以期待move()会使其参数成为容量为0的vector,从而避免复制所有元素。换言之,move有可能是破坏性的读取。

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 it deletes or overwrites it. That would be unmanageable.

在这个特定的例子中,我们也可以使用特化来优化swap。然而,我们无法特化每一个拷贝一个大对象,然后将其删除或覆写的函数。那是做不到的。

The first task of rvalue references is to allow us to implement move() without verbosity, or runtime overhead.

右值引用的第一项任务就是允许我们实现move()而无须使用冗长的代码或引入运行时开销。

move

move

The move function really does very little work. All move does is accept either an lvalue or rvalue argument, and return it as an rvaluewithout triggering a copy construction:

move函数完成的工作实际上微乎其微。它所做的就是接受一个左值或右值参数,然后将它作为右值返回而不引发拷贝构造:

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 is an lvalue or rvalue (e.g. copy constructor and assignment operator). When the argument is an lvalue, the argument must be copied from. When it is an rvalue, it can safely be moved from.

然后,由客户端代码负责根据参数是左值或右值的不同情形来重载关键函数(如拷贝构造函数和拷贝赋值运算符)。参数是左值时必须拷贝它,是右值时则可以安全地转移它。

Overloading on lvalue / rvalue

针对左值/右值的重载

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

考虑一个简单的持有某种资源同时提供拷贝语义(拷贝构造函数和赋值运算符)的句柄类。比如,一个clone_ptr可以拥有一个指针,并基于指针调用clone()来达到拷贝对象的目的。

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_ptr is code that you might find in today's books on C++. Clients of clone_ptr might use it like so:

上面的clone_ptr代码中,除了特别指明的用于转移语义的部分之外,其它都是可以在今天的各种C++书上找到的代码。clone_ptr的客户可以这样使用它:

clone_ptr p1(new derived);
// ...
clone_ptr 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 potentially expensive 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 constructed state. Themove assignment operator simply swaps state with the rvalue.

注意,clone_ptr的拷贝构造和赋值是开销较大的操作。然而,如果确知拷贝源是右值,我们可以直接偷走那个右值的指什(没有人会注意到),从而避免潜在的大开销clone()操作。上面的转移构造函数就是这么做的,把被拷贝的右值置成了默认构造的状态。转移赋值运算符则直接跟右值交换了状态。

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

现在,当代码尝试拷贝一个clone_ptr右值,或者显式地允许将拷贝源看作右值(通过std::move),操作执行起来会快很多。

clone_ptr p1(new derived);
// ...
clone_ptr p2 = std::move(p1);  // p2 now owns the pointer instead of p1

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

对于由其它类构成的类(通过包含或继承)来说,使用std::move函数,其转移构造函数和转移赋值编写起来都很容易。

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 like our eariler clone_ptr example) which will completely avoid the tremendously more expensive copy operations.

现在,绑定到子对象的构造函数或赋值运算符时,每个子对象都被作为右值看待。std::vector和std::string已经实现了转移操作(就像之前的clone_ptr),从而完全避免了开销高出数倍的拷贝操作。

Note above that the argument x is treated as an lvalue internal to the move functions, even though it is declared as an rvalue reference parameter. That's why it is necessary to say move(x) instead of just x when passing down to the base class. This is a key safety feature of 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 using std::move. If you have a name for the variable, it is an lvalue.

注意,上面的x虽然声明为右值引用参数,但转移函数内部却把它看成左值。这就是为什么在将它进一步传给基类的时候我们使用了move(x)而不直接使用x。这是转移语义的一项重要的安全特性,其目的是避免对命名变量做意外的重复拷贝。所有的转移要么发生在右值上,要么应显式地转换成右值,比如使用std::move。命名的变量都是左值。

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

问题:不占资源的类型又是什么情况呢?(比如std::complex?)

No work needs to be done in that case. The copy constructor is already optimal when copying from 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 tremendously increased. Movable but non-copyable types can be returned by value from factory functions:

有些类型不适用拷贝语义,但仍然是可转移的。比如:

·         fstream

·         unique_ptr(非共享、不可拷贝的所有权)

·         代表线程的类型

让这样的类型变得可转移(虽仍不能拷贝)可大大提高其效用。可转移但不可拷贝的类型可从工厂方法中按值返回:

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 long as the source ifstream is an rvalue. At all times, there is still only one underlying file handle, and only one ifstream owns it at a time.

上面的例子中,只要源ifstream是右值,底层的文件句柄就会从一个对象传到另一个对象。任何时候底层的文件句柄仍然只有一个,且每一时刻只有一个ifstream对象拥有它。

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

可转移但不可拷贝的类型也可以安全地放进标准容器中。如果容器内部需要“拷贝”一个元素(比如vector重新分配内存),它会转移元素而不会拷贝元素。

vector<unique_ptr<base>> v1, v2;
v1.push_back(unique_ptr(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 as opposed to copying them. This not only provides better performance (like the improved std::swap implementation described above), but also allows these algorithms to operate on movable but non-copyable types. For example the following code sorts a vector<unique_ptr<T>> based on comparing the pointed-to types:

序列中的元素可以转移而无需拷贝,这可使许多标准算法受益。这不仅带来了更高的性能(就像之前描述的更好的std::swap实现),还让这些算法能够操作可转移但不可拷贝的类型。比如,下面的代码基于被指向的类型来给一个vector<unique_ptr<T>>排序:[译注:根据C++11新标准,嵌套模板参数无须再在两个“>”之间加上一个空格。]

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 use swap (which no longer requires Copyability) or move construction / move assignment. Thus during the entire algorithm, the invariant that each item is owned and referenced by one and only one smart pointer is maintained. If the algorithm were to attempt a copy (say, by programming mistake) a compile time error would result.

当sort四处移动unique_ptr时,它将使用swap(swap不再要求被交换的对象“可拷贝”)或转移构造/转移赋值。于是,在整个算法中,“每个项目被一个且仅有一个智能指针拥有并引用”的不变式得以维持。算法如果试图拷贝项目(比如出于程序错误)将引发编译错误。

Perfect Forwarding

完美转发

Consider writing a generic factory function that returns a std::shared_ptr for a newly constructed generic type. Factory functions such as this 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:

考虑编写一个通用的工厂函数,它基于新创建的一般类型的对象返回std::shared_ptr。这样的工厂函数对于资源分配的封装和局部化意义非浅。显而易见的是:新创建对象的构造函数能接受什么参数,工厂函数也必须能接受同样的一组参数:

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?

问题:如果T的构造函数接受一个非const引用参数又当如何?

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

那种情况下我们将得到一条编译错误:工厂函数中const修饰的实参不能绑定到T构造函数中的非const形参。

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

为解决这一问题,我们在工厂函数中使用非const参数。

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, the const will be deduced into the template parameter (A1 for example) and then properly forwarded to T's constructor. Similarly, if a non-const argument is given to factory, 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).

这样就好多了。如果const修饰的类型传入factory,const将被推导到模板形参上(比如A1),然后合情合理地转发到T的构造函数上。类似地,如果非const实参传入factory,它将作为非const量正确转发到T的构造数上。实际上,这恰恰是今天的转发应用(比如std::bind)所采用的实现方式。

However, consider:

然而,考虑下面的情况:

std::shared_ptr<A> p = factory<A>(5); // error
A* 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.

这个例子在我们第一版的factory中没有问题,现在却不行了。“5”将factory的模板参数推导为int&,接下来int&却不能绑定到右值“5”。到目前,两种方案都不正确。每一种都会打破一些合理的、常见的代码。

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

问题:针对每一种AI&和const 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.

这样我们可以应付所有示例代码,代价却是指数级的爆炸:双参数的情况需要4种重载。三参数的factory需要8种重载。四参数的factory需要16种。依此类推。这不是一个可伸缩的解决方案。

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.

现在右值实参可以绑定到factory的形参了。如果实参带有const,这一事实将被推导到factory的模板参数类型上。

Question: What is that forward function in our solution?

问题:我们的解决方案中,那个forward是做什么的?

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

类似move,forward也是一个简单的库函数,也是用于直接、显式地表达意图,而不必通过可能非常神秘的引用语法。我们想要转发实参a1,于是便直接这么写。

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

在这里,forward保持了factory实参的左值/右值属性。如果右值传入factory,那么在forward函数的帮助下,传入T构造函数的也是右值。同样,如果左值传入factory,那它也将作为左值转发给T的构造函数。

The definition of forward looks like this:

forward可以这样定义:

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 above description. But the above content represents 95% of the knowledge with a fraction of the reading.

This proposal was initially put forth in the following paper. The present article is substantially a reprint of the original proposal:

Hinnant, Howard, E., Bjarne Stroustrap, and Bronek Kozicki. A Brief Introduction to Rvalue References

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2027.html

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 see N1385.

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

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

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

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

·         N1856
·         N1857
·         N1858
·         N1859
·         N1860
·         N1861
·         N1862
For a proposal to extend the rvalue reference to the implicit object parameter (this), please seeN1821.

Share your opinion

Have an opinion about Rvalue references?

Discuss this article in the Articles Forum topic, A Brief Introduction to Rvalue References.

About the Authors

Howard Hinnant is the lead author of the rvalue reference proposals for the next C++ standard. He implemented and maintained the standard C++ library for Metrowerks/Motorola/Freescale from the late 90's to 2005. He is currently a senior software engineer at Apple and serving on the C++ standards committee as Library Working Group chairman.

Bjarne Stroustrup is the designer and original implementor of the C++ Programming Language. He is currently the College of Engineering Endowed Chair in Computer Science at Texas A&M University. He formerly worked as the head of AT&T Lab's Large-scale Programming Research department, from its creation until late 2002.

Bronek Kozicki is an experienced C++ programmer. He is a member of BSI C++ panel and author of "extending move semantics to *this" proposal (N1821, evolved to N2439). Bronek currently works for a leading investment bank in London.

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值