右值引用,转移语义与完美转发

1. 左值与右值:

C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值.

可见立即数,函数返回的值等都是右值;而对象(包括变量),函数返回的引用,const对象等都是左值.

从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如:

int& foo(){int tmp; return tmp;}

int fooo(){int tmp; return tmp}

int a=10;

const int b;

int& temp=foo();

int tempp=fooo();

以上代码中,a,temp和foo()都是非常量左值,b是常量左值,fooo()是非常量右值,10是常量右值.

对于左值和右值的深入解释,详见http://blog.csdn.net/talentluke/article/details/6101748

2. 右值引用:

右值引用的表示方法为

 Datatype&& variable

右值引用是C++ 11新增的特性,所以C++ 98的引用为左值引用.右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期,右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的建构来减少对象建构和赋值操作以达到提高效率的目的,例如对于以下函数:

(Demo是一个类)
Demo foo(){
    Demo tmp;
    return tmp;
}

在编译器不进行RVO(return value optimization)优化的前提下以下操作:

Demo x=foo();

将会调用三次构造函数(tmp的,x的,r临时对象),相应的在对象被销毁时也会调用三次析构函数,而如果采用右值引用的方式:

Demo&& x=foo();

那么就不需要进行x的建构,本来本来要被销毁的临时对象也会由于x的绑定而将生存期延长至和x一样(可以理解为x赋予了那个临时对象一个合法地位:一个名字),就需要提高了效率(代价就是tmp需要占据4字节空间,但这是微不足道的).

右值引用与左值引用绑定规则:

常量左值引用可以绑定到常量和非常量左值,常量和非常量右值;

非常量左值引用只能绑定到非常量左值;

非常量右值引用只能绑定到非常量右值(vs2013也可以绑定到常量右值);

常量右值引用只能绑定到常量和非常量右值(非常量右值引用只是为了语义的完整而存在,常量左值引用就可以实现它的作用).

虽然从绑定规则中可以看出常量左值引用也可以绑定到右值,但显然不可以改变右值的值,右值引用就可以,从而实现转移语义,因为右值引用通常要改变所绑定的右值,所以被绑定的右值不能为const.

注意:右值引用是左值!

3. 转移语义(move semantics):

右值引用被引入的目的之一就是实现转移语义,转移语义可以将资源 ( 堆,系统对象等 ) 的所有权从一个对象(通常是匿名的临时对象)转移到另一个对象,从而减少对象构建及销毁操作,提高程序效率(这在2的例子中已经作了解释).转移语义与拷贝语义是相对的.从转移语义可以看出,实际上,转移语义并不是新的概念,它实际上已经在C++98/03的语言和库中被使用了,比如在某些情况下拷贝构造函数的省略(copy constructor elision in some contexts),智能指针的拷贝(auto_ptr “copy”),链表拼接(list::splice)和容器内的置换(swap on containers)等,只是还没有统一的语法和语义支持(参考自http://book.2cto.com/201306/25365.html)

虽然普通的函数和操作符也可以利用右值引用实现转移语义(如2中的例子),但转移语义通常是通过转移构造函数和转移赋值操作符实现的.转移构造函数的原型为Classname(Typename&&),而拷贝构造函数的原型为Classname(const Typename&),转移构造函数不会被编译器自动生成,需要自己定义,只定义转移构造函数也不影响编译器生成拷贝构造函数,如果传递的参数是左值,就调用拷贝构造函数,反之,就调用转移构造函数.

例如:

class Demo{

public:

    Demo():p(new int[10000]{};

    Demo(Demo&& lre):arr(lre.arr),size(lra.size){lre.arr=NULL;}//转移构造函数

    Demo(const Demo& lre):arr(new int[10000]),size(arr.size){

        for(int cou=0;cou<10000;++cou)

            arr[cou]=lew.arr[cou];

    }

private:

    int size;

    int* arr;

}

从以上代码可以看出,拷贝构造函数在堆中重新开辟了一个大小为10000的int型数组,然后每个元素分别拷贝,而转移构造函数则是直接接管参数的指针所指向的资源,效率搞下立判!需要注意的是转移构造函数实参必须是右值,一般是临时对象,如函数的返回值等,对于此类临时对象一般在当行代码之后就被销毁,而采用转移构造函数可以延长其生命期,可谓是物尽其用,同时有避免了重新开辟数组.对于上述代码中的转移构造函数,有必要详细分析一下:

Demo(Demo&& lre):arr(lre.arr),size(lre.size)({lre.arr=NULL;}

lre是一个右值引用,通过它间接访问实参(临时对象)的资源来完成资源转移,lre绑定的对象(必须)是右值,但lre本身是左值;

因为lre是临时对象,”lre.arr=NULL"必不可少,否则函数结尾调用析构函数销毁lre时仍然会将资源释放,转移的资源还是被系统收回.

4. move()函数

3中的例子并非万能,Demo(Demo&& lre)的实参必须是右值,有时候一个左值即将到达生存期,但是仍然想要使用转移语义接管它的资源,这时就需要move函数.

std::move函数定义在标准库<utility>中,它的作用是将左值强行转化为右值使用,从实现上讲,std:move等同于static_cast<T&&>(lvalue),由此看出,被转化的左值本身的生存期和左值属性并没有被改变,这类似于const_cast函数.因此被move的实参应该是即将到达生存期的左值,否则的话可能起到反面效果.

5. 完美转发(perfect forwarding)(参考自http://book.2cto.com/201306/25369.html)

完美转发指的是将一组实参"完美"地传递给形参,完美指的是参数的const属性与左右值属性不变,例如在进行函数包装的时候,func函数存在下列重载:

void func(const int);
void func(int);
void func(int&&);

如果要将它们包装到一个函数cover内,以实现:

void cover(typename para){
    func(para);
}

使得针对不同实参能在cover内调用相应类型的函数,似乎只能通过对cover进行函数重载,这使代码变得冗繁,另一种方法就是使用函数模板,但在C++ 11之前,实现该功能的函数模板只能采用值传递,如下:

template<typename T>
void cover(T para){
    ...
    func(para);
    ...
}
    

但如果传递的是一个相当大的对象,又会造成效率问题,要通过引用传递实现形参与实参的完美匹配(包裹const属性与左右值属性的完美匹配),就要使用C++ 11 新引入的引用折叠规则:

函数形参       T的类型         推导后的函数形参

T&               A&                A&
T&               A&&              A&
T&&             A&                A&
T&&             A&&              A&&

因此,对于前例的函数包装要求,采用以下模板就可以解决:

template<typename T>
void cover(T&& para){
    ...
    func(static_cast<T &&>(para));
    ...
}

如果传入的是左值引用,转发函数将被实例化为:

void func(T& && para){

    func(static_cast<T& &&>(para));

}

应用引用折叠,就为:

void func(T& para){

    func(static_cast<T&>(para));

}

如果传入的是右值引用,转发函数将被实例化为:

void func(T&& &&para){

     func(static_cast<T&& &&>(para));
}

应用引用折叠,就是:

void func(T&&  para){

    func(static_cast<T&&>(para));

}

对于以上的static_cast<T&&>,实际上只在para被推导为右值引用的时候才发挥作用,由于para是左值(右值引用是左值),因此需要将它转为右值后再传入func内,C++ 11在<untility>定义了一个std::forward<T>函数来实现以上行为,

所以最终版本为

template<typename T>

void cover(T&& para){

    func(forward(forward<T>(para)));

}

std::forward的实现与static_cast<T&&>(para)稍有不同,具体可见http://www.zhihu.com/question/34544004

std::forward函数的用法为forward<T>(para),若T为左值引用,para将被转换为T类型的左值,否则para将被转换为T类型右值

时间: 2024-11-05 20:33:07

右值引用,转移语义与完美转发的相关文章

详解C++右值引用

http://jxq.me/2012/06/06/%E8%AF%91%E8%AF%A6%E8%A7%A3c%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8/#thbecker C++0x标准出来很长时间了,引入了很多牛逼的特性[1].其中一个便是右值引用,Thomas Becker的文章[2]很全面的介绍了这个特性,读后有如醍醐灌顶,翻译在此以便深入理解. 目录 概述 move语义 右值引用 强制move语义 右值引用是右值吗? move语义与编译器优化 完美转发:问题

深入右值引用,move语义和完美转发

深入右值引用,move语义和完美转发 转载请注明:http://blog.csdn.net/booirror/article/details/45057689 乍看起来,move语义使得你可以用廉价的move赋值替代昂贵的copy赋值,完美转发使得你可以将传来的任意参数转发给 其他函数,而右值引用使得move语义和完美转发成为可能.然而,慢慢地你发现这不那么简单,你发现std::move并没有move任何东西,完美转发也并不完美,而T&&也不一定就是右值引用-- move语义 最原始的左值

C++ 右值引用:移动语义与完美转发

转载至: http://www.dutor.net/index.php/2013/11/rvalue-reference-move-semantics-and-perfect-forwarding/ C++11 引入的新特性中,除了并发内存模型和相关设施,这些高帅富之外,最引人入胜且接地气的特性就要属『右值引用』了(rvalue reference).加入右值引用的动机在于效率:减少不必要的资源拷贝.考虑下面的程序: 1 2 std::vector<string> v; v.push_back

[转][c++11]我理解的右值引用、移动语义和完美转发

c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象.所有的具名变量或者对象都是左值,而右值不具名.很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值. 看见书上又将右值分为将亡值和纯右值.纯右值就是c++98标准中右值的概念,

[c++11]右值引用、移动语义和完美转发

c++中引入了右值引用和移动语义,可以避免无谓的复制,提高程序性能.有点难理解,于是花时间整理一下自己的理解. 左值.右值 C++中所有的值都必然属于左值.右值二者之一.左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象.所有的具名变量或者对象都是左值,而右值不具名.很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值. 看见书上又将右值分为:将亡值和纯右值. 纯右值就是c++98标准中右值的概

Effective Modern C++:05右值引用、移动语义和完美转发

移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引用是将这两个不相关的语言特性连接起来的底层语言机制,正是它使得移动语义和完美转发成了可能. 23:理解std::move和std::forward std::move并不进行任何移动,std::forward也不进行任何转发.这两者在运行期都无所作为,它们不会生成任何可执行代码.实际上,std::m

[C++]右值引用和转移语义

右值引用和转移语义 本文尝试着解释何为右值引用和转移语义以及使用它们具有优势,并提供相关案例分析. 定义 左值和右值 首先我们先来理解一下什么是左值和右值. C/C++语言中可以放在赋值符号左边的变量,左值表示存储在计算机内存的对象,左值相当于地址值.右值:当一个符号或者常量放在操作符右边的时候,计算机就读取他们的"右值",也就是其代表的真实值,右值相当于数据值. C/C++语言中可以放在赋值符号左边的变量,即具有对应的可以由用户访问的存储单元,并且能够由用户去改变其值的量.左值表示存

C++11 标准新特性: 右值引用与转移语义

C++ 的新标准 C++11 已经发布一段时间了.本文介绍了新标准中的一个特性,右值引用和转移语义.这个特性能够使代码更加简洁高效. 查看本系列更多内容 | 3 评论: 李 胜利, 高级开发工程师, IBM 2013 年 7 月 10 日 内容 在 IBM Bluemix 云平台上开发并部署您的下一个应用. 开始您的试用 新特性的目的 右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move

c++11 右值引用与转移语义

右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding).它的主要目的有两个方面: 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率. 能够更简洁明确地定义泛型函数. 左值与右值的定义 C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值.通俗的左值的定义就是非临时对象,那些可以在多条语句中使