注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

还东国的博客

行之苟有恒,久久自芬芳

 
 
 

日志

 
 

(转载)右值引用---c++0x新特性  

2013-04-17 16:35:00|  分类: C++(VC)编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

右值引用

http://blog.csdn.net/candy060403/article/details/7414456
分类: C++ 2012-03-31 10:10 157人阅读 评论(0) 收藏 举报

关于C++11右值引用的一篇译文。在转载过程中对文章进行了排版上的一些编辑,其他内容未动。

 

原文链接:

01.http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

02.http://cpp-next.com/archive/2009/09/move-it-with-rvalue-references/

03.http://cpp-next.com/archive/2009/09/making-your-next-move/

04.http://cpp-next.com/archive/2009/09/your-next-assignment/

05.http://cpp-next.com/archive/2009/10/exceptionally-moving/

06.http://cpp-next.com/archive/2009/12/onward-forward/

 

译文链接:

01.http://blog.csdn.net/alai04/article/details/6618502

02.http://blog.csdn.net/alai04/article/details/6625754

03.http://blog.csdn.net/alai04/article/details/6627954

04.http://blog.csdn.net/alai04/article/details/6656234

05.http://blog.csdn.net/alai04/article/details/6719603

06.http://blog.csdn.net/alai04/article/details/6724345

 

 

第一篇:想要快?就传值

 

实话实说,你对以下这段代码有何感觉?

  1. std::vector<std::string> get_names();  
  2. …  
  3. std::vector<std::string> const names = get_names();  

坦白的说,虽然我知道没那么糟,但是我还是感觉不妙。原则上,当 get_names() 返回时,我们必须复制一个含有多个 string 的 vector。然后,我们在初始化 names 的时候还要再一次复制它,最后我们还要销毁第一份拷贝。如果在 vector 中有N个 string,那么每次复制可能需要多至N+1次内存分配,而且 string 内容的复制会导致一系列缓存失效的数据访问。

为了消除这种顾虑,我通常会使用传引用的方法来避免无用的复制:

  1. get_names(std::vector<std::string>& out_param );  
  2. …  
  3. std::vector<std::string> names;  
  4. get_names( names );  

不幸的是,这种做法也很不理想。

  • 代码增长了150%。
  • 我们必须去掉 const,因为我们要修改 names。
  • 正如函数式编程的程序员经常提醒我们的,函数参数可被改写会使代码变得复杂,原因是它破坏了引用透明性和方程式推理。
  • 对于 names,我们失去了严格的值语义。

难道真的必须这样来写代码才可以提高效率吗?幸好,答案是不必如此(特别是当你使用的是C++0x时)。我们有一系列文章探讨右值以及它对于提高C++值语义效率的影响,本文是这个系列中的第一篇。

右值

右值是指创建匿名临时对象的表达式。右值的名字来自这样一个事实,内置类型的右值表达式只能出现在赋值操作符的右侧。这一点和左值不同,不带 const 的时候,左值是可以出现在赋值操作符的左侧的,右值表达式生成的对象没有任何持久的标识用来向它赋值。

不过,我们要讨论的是匿名临时对象的另一个重要特性,就是它们可以在表达式中只使用一次。你怎么可能再一次提及这样的一个对象呢?它没有名字(即“匿名”);而且在整个表达式求值完毕后,对象即被销毁(即“临时”)!

如果你知道你是从一个右值进行复制的话,你就有可能从源对象处将复制开销较高的资源“偷过来”,在目标对象中使用它们而不会有任何人留意它。在前面的例子中,就是将源 vector 中动态分配的字符串数组的所有权传递给目标 vector。如果我们可以在某种程度上让编译器来为我们执行这种“转移”操作,那么从以传值方式返回的 vector 来初始化 names 的代价就非常低——几乎为零。

以上是关于第二次复制的,那么第一次复制呢?原则上,当 get_names 返回时,必须将函数的返回值从函数的内部复制到外部。很好,返回值具有与匿名临时对象一样的特性:它们马上就会被销毁,以后也不会再被用到。所以,我们可以用相同的方法来消除掉第一次复制,将资源从函数内部的返回值处转移给函数调用者可见的匿名临时对象。

复制省略和RVO

前面提到复制的时候,我都写上“原则上”,其原因是,实际上编译器都允许基于我们已经讨论过的那些原则来执行一些优化。这一类优化通常被称为复制省略。例如在返回值优化(RVO)中,调用者函数在其栈上分配空间,然后将这块内存的地址传给被调用函数。被调用函数可以在这块内存上直接构造返回值,以消除从函数内部至外部的复制。该复制被编译器省略,或者说“消掉”。因此在以下代码中,将不需要进行复制:

  1. std::vector<std::string> names = get_names();  

同样,当一个函数参数以传值方式传递时,虽然编译器通常被要求建立一份拷贝(所以在函数内部修改该参数不会影响到调用者),但是当源对象是右值时,也允许省略这个复制而直接使用源对象本身。

  1. std::vector<std::string>   
  2. sorted(std::vector<std::string> names)  
  3. {  
  4.     std::sort(names);  
  5.     return names;  
  6. }  
  7.    
  8. // names is an lvalue; a copy is required so we don't modify names  
  9. std::vector<std::string> sorted_names1 = sorted( names );  
  10.    
  11. // get_names() is an rvalue expression; we can omit the copy!  
  12. std::vector<std::string> sorted_names2 = sorted( get_names() );  

这真的很了不起。原则上,编译器可以消除在第12行中所有令人担心的复制,使得 sorted_names2 与 get_names() 中所创建的对象是同一个对象。但是在实践中,这一原则不会走得象我们所想的那么远,其原因我稍后解释。

启示

虽然复制省略从未被标准要求实现,但是我已测试过的每一个编译器的最新版本都已实现此种优化。即使你对于以传值方式返回那些重量级对象感到不舒服,复制省略还是会改变你编写代码的方式。

我们来看一下前面那个组 sorted(...) 函数的以下写法,它接受以 const 引用方式传入的 names 并进行一次显式的复制:

  1. std::vector<std::string>   
  2. sorted2(std::vector<std::string> const& names) // names passed by reference  
  3. {  
  4.     std::vector<std::string> r(names);        // and explicitly copied  
  5.     std::sort(r);  
  6.     return r;  
  7. }  

虽然乍看起来 sorted 和 sorted2 是一样的,但是如果编译器实现了复制省略,它们会有巨大的性能差异。即便传给 sorted2 的实参是右值,进行复制的源对象 names 也是一个左值,因此复制不能被优化掉。从某种意义上说,复制省略是分离编译模式的牺牲品:在 sorted2 函数体内部,没有任何关于传给函数的实参是否为右值的信息;而在外部的调用点,也没有迹象显示该实参最终会被复制。

这一事实直接将我们引至出以下指引:

指引:不要复制你的函数参数。而应该以传值的方式来传递它,让编译器来做复制。

最坏的情况下,如果你的编译器不支持复制省略,性能也不会更坏。而最好的情况下,你会看到性能的极大提升。

你可以立即应用该指引的一个地方就是赋值操作符。规范的、易写的、保证正确的、强异常保证的、复制并交换的赋值操作符通常会这样写:

  1. T& T::operator=(T const& x) // x is a reference to the source  
  2. {   
  3.     T tmp(x);          // copy construction of tmp does the hard work  
  4.     swap(*this, tmp);  // trade our resources for tmp's  
  5.     return *this;      // our (old) resources get destroyed with tmp   
  6. }  

但是通过以上对复制省略的讨论,可以知道这种写法显然是低效的!显而易见,现在正确编写一个复制并交换的赋值操作应该是:

  1. T& operator=(T x)    // x is a copy of the source; hard work already done  
  2. {  
  3.     swap(*this, x);  // trade our resources for x's  
  4.     return *this;    // our (old) resources get destroyed with x  
  5. }  

真的假不了

当然,天下没有免费的午餐,所以我还有以下说明。

首先,当你以引用方式传递一个参数并在函数体内对其进行复制时,复制构造函数是从一个集中的地方被调用的。但是,当你以传值方式传递一个参数时,编译器为其生成的对复制构造函数的调用是位于每一次对左值进行传递的调用点。如果该函数在多个地方被调用,且代码大小或局部性是你的应用程序的关键重点,这的确会是一个问题。

另一方面,也可以很容易地建立一个包装函数,将复制局部化:

  1. std::vector<std::string>   
  2. sorted3(std::vector<std::string> const& names)  
  3. {  
  4.     // copy is generated once, at the site of this call  
  5.     return sorted(names);  
  6. }  

由于反之并不成立——你不能通过包装来取回已失去的复制省略的机会——所以我建议你还是要从前面的指引开始,然后仅在发现必须要做的时候才改变它。

其次,我还没有发现有哪个编译器可以在函数返回其参数时进行复制省略,正如我们的 sorted 实现。你可以想象一下如何进行这些复制省略:没有某种形式的跨函数优化,sorted 的调用者无从知晓其参数(而不是其它对象)最终会被返回,所以编译器必须在栈上分别为参数和返回值分配不同的空间。

如果你要返回一个函数的参数,你还是可以获得近似最优的性能,方法是与一个缺省构造的返回值进行交换(所提供的缺省构造函数和交换函数必须该是低开销的,通常也是如此):

  1. std::vector<std::string>   
  2. sorted(std::vector<std::string> names)  
  3. {  
  4.     std::sort(names);  
  5.     std::vector<std::string> ret;  
  6.     swap(ret, names);  
  7.     return ret;  
  8. }   

第二篇:用右值引用来转移

这是关于C++中的高效值类型的系列文章中的第二篇。在上一篇中,我们讨论了复制省略如何被用来消除可能发生的多次复制操作。复制省略是透明的,在看起来非常普通的代码中自动发生的,几乎没有任何缺点。好消息已经够多了;下面看看坏的消息:

  1. 复制省略不是标准强制要求的,因此你写不出可以保证它会发生的可移植代码。
  2. 有些时候这也做不到。例如:
    1. return q ? var1 : var2;  
    被调用者最多可以将调用者传入的内存用于 var1 或 var2 其中一个。如果它选择将 var1 保存在该内存中,而 q 为 false,那么 var2 还是要被复制(反之亦然)。
  3. 复制省略很有可能超出编译器的栈空间分配技巧的能力。

低效转移

当一个操作是要对数据进行重排时,有很多机会可以进行优化。以一个简单的泛型插入排序算法为例:

  1. template <class Iter>                                                    
  2. void insertion_sort(Iter first, Iter last)                                
  3. {                                                                         
  4.     if (first == last) return;                                            
  5.    
  6.     Iter i = first;                                                       
  7.     while (++i != last)     // Invariant: elements preceding i are sorted   
  8.     {                                                                     
  9.         Iter next = i, prev = i;                                          
  10.         if (*--prev > *i)                                                 
  11.         {                                                                 
  12.             typename std::iterator_traits<Iter>::value_type x(*next);    
  13.             do *next = *prev;  
  14.             while(--next != first && *--prev > x);                        
  15.             *next = x;  
  16.         }                                                                 
  17.     }                                                                     
  18. }        


第7行:外层循环的不变式


 第12行:将第一个未排序元素复制至临时位置

  

   第13行:将最后一个已排序元素复制向后复制


第13行:继续向后复制,直至找到合适位置


第15行:将临时位置的元素复制至正确位置

想象一下,如果进行排序的序列中的元素是 std::vector<std::string>,会发生什么:在第12、13、15行,我们要潜在地复制一个字符串 vector,这会导致大量的内存分配和数据复制。

由于排序操作从根本上说,是一种数据守恒的操作,所以这些数据复制的开销应该都是可避免的:原则上,我们真正要做的就是将对象在序列中移来移去。

对于这些高代价的复制操作,要留意的一个重点是,在所有情况下,源对象的值都不会再被使用。听起来很熟悉吧?是的,当源对象是右值时也是如此。不过这次源对象是左值:这些对象都是有地址的。

引用计数可以吗?

解决这类低效问题的一个常用方法是,在堆上分配元素并在序列(容器)中持有指向这些元素的引用计数智能指针,而不是直接保存这些元素。引用计数智能指针跟普通的指针类似,只是它还跟踪了有多少引用计数智能指针指向同一个对象,并且会在最后一个智能指针被删时销毁对象。复制一个引用计数指针只需递增其引用计数即可,这是很快的。对引用计数指针赋值则是递增一个引用计数且递减另一个。这也是很快的。

那么,还可以更快吗?当然是根本就不进行计数!另外,引用计数还有其它一些我们希望避免的弱点:

  1. 它的开销在多线程环境中是非常大的,因为计数本身要跨线程共享,这就需要同步。
  2. 在泛型代码中该方法要失效,因为元素的类型有可能是象 int 这样的轻量型类型。在这种情况下,引用计数的增减才是真正的性能开销。你要么忍受这种开销,要么就必须引入一个复杂的框架来确定哪些类型是轻量型的,应该直接保存,同时还要以统一的风格来访问这些值。
  3. 引用语义会使得代码难以阅读。例如:
     
    1. typedef std::vector<std::shared_ptr<std::string> > svec;  
    2. …  
    3. svec s2 = s1;  
    4. std::for_each( s2.begin(), s2.end(), to_uppercase() );  
    将 s2 变为大写同时会修改到 s1 的值。这是一个比我们在这里讨论的要大得多的主题,简而言之,当数据共享被隐藏时,看似局部的修改,其效果却不一定是完全局部的。

引入C++0x的右值引用

为了解决这些问题,C++0x 引入了一种新的引用,右值引用。T 的右值引用写作 T&&(读作“tee ref-ref”),我们现在将原来的 T& 引用称为“左值引用”。就我们讨论的范围而言,左值引用与右值引用的主要区别在于,非 const 的右值引用可以绑定至右值。许多C++程序员都曾经遇到过这样的错误提示:

  1. invalid initialization of non-const reference of type 'X&'   
  2. from a temporary of type 'X'  

这类提示通常是由以下这样的代码引起的:

  1. X f();            // call to f yields an rvalue   
  2. int g(X&);  
  3. int x = g( f() ); // error  

标准规定,非 const 的(左值)引用应绑定至一个左值,而不是一个临时对象(即一个右值)。这是有意义的,因为对引用所引向的临时对象进行的任何修改都肯定会丢失。与之相反,非 const 右值引用应绑定至一个临时对象,而不是一个左值:

  1. X f();  
  2. X a;  
  3. int g(X&&);  
  4.    
  5. int b = g( f() ); // OK   
  6. int c = g( a );   // ERROR: can't bind rvalue reference to an lvalue  

偷取资源

假设我们的函数 g() 要保存一份它的参数的拷贝,以备后用:

  1. static X cache;  
  2.    
  3. int g(X&& a)  
  4. {  
  5.     cache = a;    // keep it for later   
  6. }  
  7.    
  8. int b = g( X() ); // call g with a temporary  

依赖于类型 X,这个复制可能是开销很大的操作,可能引起内存分配和许多子对象的深度复制。

由于 g() 的参数是一个右值引用,我们知道它只能自动绑定到匿名临时对象,而不是其它对象。因此,

  1. 在我们把这个临时对象复制到 cache 之后不久,被复制的源对象就会被销毁。
  2. 我们对这个临时对象的任何修改,对于程序的其它地方都是不可见的。

这给了我们一个机会来执行一些新的优化,通过修改临时对象的值来避免多余的工作。最为常见的一种优化就是资源偷取。

资源偷取是指从一个对象取走资源(如内存、大的子对象)并将资源转移给另一个对象。例如,string 类可能拥有一个在堆上分配的字符缓冲区。复制一个 string 需要分配一块新的缓冲区并将所有字符复制到新的缓冲区中,这看起来很慢。而偷取一个 string 则只需要让另一个对象取走这个 string 的缓冲区并通知源对象它不再拥有有效的缓冲区——这个操作要快很多。使用右值引用,我们可以通过把从临时对象复制改为从临时对象偷取,来优化我们的代码。同时,由于只有临时对象被改变,所以这个优化在逻辑上是无改写的。

说明:从右值引用偷取(或修改)可以在逻辑上视为无改写操作。

右值重载用法

从以上说明我们可以得到一个新的维持语义的编程变化:我们可以用另一个在同一位置接受右值引用的版本来对接受一个(const)引用参数的任意函数进行重载:

  1. void g(X const& a) { … }    // doesn't mutate argument   
  2. void g(X&& a) { modify(a); } // new overload; logically non-mutating  

g 的第二个重载版本可以修改它的参数,但不会对程序的其它地方产生影响,所以它具有与第一个重载版本相同的语义。

绑定与重载

下表总结了 C++0x 对于引用绑定与重载的完整规则:

表达式→

引用类型↓
T 右值 const T 右值 T 左值 const T 左值 优先级
T&& X       4
const T&& X X     3
T&     X   2
const T& X X X X 1

“优先级”一列描述了这些引用在重载决议中的行为。例如,给出以下重载:

  1. void f(int&&);        // #1   
  2. void f(const int&&);  // #2   
  3. void f(const int&);   // #3  

把一个 const int 类型的右值传入 f,将会调用#2,因为#1无法绑定而#3的优先级较低。

声明一个可转移的类型

有了以上方法,我们可以通过两个新的操作,转移构造和转移赋值,来令任意类型的右值成为可隐式转移的,这两个操作都是接受右值引用参数。例如,一个可转移的 std::vector 在 C++0x 中可能会这样写:

  1. template <class T, class A>  
  2. struct vector  
  3. {  
  4.     vector(vector const& lvalue);            // copy constructor   
  5.     vector& operator=(vector const& lvalue); // copy assignment operator   
  6.     vector(vector&& rvalue);                 // move constructor   
  7.     vector& operator=(vector&& rvalue);      // move assignment operator   
  8.     …  
  9. };  

转移构造函数和转移赋值操作符的工作就是从它的参数中“偷取”资源,然后将参数置于一个可析构或可赋值的状态。

在 std::vector 的例子中,这可能意味着将其参数置回空容器的状态。一个典型的 std::vector 实现包含有三个指针:一个指向已分配空间的起始,一个指向最后一个元素,还有一个指向已分配空间的结尾。所以,当容器为空时,这三个指针均为 null,转移构造函数会象这样:

  1. vector(vector&& rhs)   
  2.   : start(rhs.start)                // adopt rhs's storage   
  3.   , elements_end(rhs.elements_end)  
  4.   , storage_end(rhs.storage_end)  
  5. {    // mark rhs as empty.   
  6.      rhs.start = rhs.elements_end = rhs.storage_end = 0;  
  7. }  

而转移赋值操作符可能会是这样:

  1. vector& operator=(vector&& rhs)  
  2. {   
  3.     std::swap(*this, rhs);  
  4.     return *this;  
  5. }  

由于右值参数会马上被销毁,所以交换操作不仅获取它的资源,同时还将我们本来拥有的资源“安排好”准备销毁。

注意:先别太高兴,这个转移赋值操作符还不是很正确。

右值引用与复制省略

std::vector 的转移构造函数的开销非常低(大约只有对内存的3次读和6次写),但也还不是免费的。幸好,标准列明了复制省略(这是真正无代价的)的优先级高于转移操作。当你把一个右值以传值方式进行传递时,或是从某个函数返回一个值时,编译器首先应选择消除复制。如果复制不能被消除,而相应的类型又具有转移构造函数,编译器就被要求使用转移构造函数。最后,如果连转移构造函数都没有,编译器就只能使用复制构造函数了。

举例:

  1. A compute(…)  
  2. {  
  3.     A v;  
  4.     …  
  5.     return v;  
  6. }  
  1. 如果 A 具有可访问的复制构造函数或转移构造函数,则编译器可以选择消除复制
  2. 否则,如果 A 具有转移构造函数,则 v 被转移
  3. 否则,如果 A 具有复制构造函数,则 v 被复制
  4. 否则,编译器报错

因此,上一篇文章中的指引依然有效:

指引:不要复制你的函数参数。而应该以传值的方式来传递它,让编译器来做复制。

以这个指引的提示下,你可能会问:“除了转移构造函数和转移赋值操作符,我还可以在哪里使用右值重载用法呢?一旦我的所有类型都是可转移的,那么还有什么要做的呢?”请看以下例子。

从左值转移

所有的这些转移优化都具有一个共通点:当我们不再使用源对象时才可以进行优化。但是有些时候,我们需要提醒一下编译器。例如:

  1. void g(X);  
  2.    
  3. void f()  
  4. {  
  5.     X b;  
  6.     g(b);  
  7.     …  
  8.     g(b);  
  9. }  

在第8行中,我们以一个左值来调用 g,这样就不能进行资源偷取——即使我们知道 b 已不会再被用到。为了告诉编译器可以从 b 进行转移,我们可以用 std::move 来传递它:

  1. void g(X);  
  2.    
  3. void f()  
  4. {  
  5.     X b;  
  6.     g(b);              // still need the value of b   
  7.     …  
  8.     g( std::move(b) ); // all done with b now; grant permission to move   
  9. }  

注意,std::move 本身并不做任何转移。它只是将参数变为一个右值引用,以便于在符合“转移优化”的环境中可以采用转移优化。当你看到 std::move 时,你可以这样想:授予转移的权限。你也可以将 std::move(a) 看作是 static_cast<X&&>(a) 的描述方式。

高效转移

现在我们有办法对左值进行转移了,我们可以将前几节中的 insertion_sort 算法优化一下:

  1. template   
  2. void insertion_sort(Iter first, Iter last)   
  3. {   
  4.     if (first == last) return;   
  5.   
  6.   
  7.     Iter i = first;   
  8.     while (++i != last)     // Invariant: [first, i) is sorted    
  9.     {   
  10.         Iter next = i, prev = i;   
  11.         if (*--prev > *i)  
  12.         {  
  13.             typename std::iterator_traits::value_type   
  14.               x( std::move(*next) );  
  15.             do *next = std::move(*prev);  
  16.             while(--next != first && *--prev > x);  
  17.             *next = std::move(x);  
  18.         }  
  19.     }  
  20. }  

  

第12行:将第一个未排序元素移至临时位置   


第13行:将最后一个已排序元素向后移

                

第13行:继续后移


第15行:将临时位置中的元素移至正确位置

除了格式上的差异以外,这个版本与前一个的区别仅在于增加了对 std::move 的调用。值得指出的是,我们只需要这一个 insertion_sort 的实现,不论元素类型是否具有转移构造函数。这是典型的可转移代码:右值引用的设计是让你“在可以的时候转移,有必须的时候复制”。 

 字数限制,见后续。


  评论这张
 
阅读(1544)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017