右值引用详解 |
您所在的位置:网站首页 › 右值引用作为函数参数的条件是什么 › 右值引用详解 |
何谓右值右值引用右值引用与其他对比右值引用与移动语义右值引用与std::move移动语义与std::move移动语义注意事项移动语义与swap完美转发
何谓右值
一个最简单判断左值、右值的方式是:等号左边的值即左值,等号右边的值即右值;但为了更加严谨,我们这样定义:能够取地址的,有名字的就是左值;反之,不能取地址,没有名字的就是右值。 右值引用右值引用简单理解,就是绑定到左值的引用,右值引用的特点是:它将让其所绑定的右值重获新生,即使该右值本身是一个临时变量,但是它本身却不能绑定任何左值。比如: int c; int && d = c; //错误,右值不能绑定左值 T && a = ReturnRvalue(); //#1 T b = ReturnRvalue(); //#2在#1中,我们声明了一个名为a的右值引用,其值等于ReturnRvalue()返回的临时变量的值。而#2,b则是通过函数返回的临时量再进行构造生成的值。 需要注意的是,上述代码的前提是:ReturnRvalue()函数本身返回的就是一个右值,所以ReturnRvalue必须类似如下形式: T && ReturnRvalue() { return T(); } 对于返回右值引用的函数来说,支持右值声明的绑定,不支持非常量左值,却支持非常量左值。具体可以参考下图: 如上所示,#1将会编译报错,#2却能编译通过,这个主要是由于c++98标准的定义问题,这里不再讨论该细节,我们只需要知道即可。 右值引用除了作为返回值,作为参数也是可以的,如下: void AcceptRvalueRef(Copyable && s) { ... }和左值引用相同,我们也不需要进行额外拷贝。 右值引用与其他对比
当右值引用作为构造函数参数时,这就是所谓的移动构造函数,也就是所谓的移动语义。 右值引用与移动语义我们知道,当类成员中存在指针成员时,使用复制拷贝构造,需要进行深拷贝,但问题在于:我们真的任何时候都需要深拷贝吗?首先来看一下下面的几个代码片段。 片段1: String s; String p = s; ...上面无疑是需要深拷贝的,因为无论s,还是p,都可能在我们后面的代码里面继续用到。 片段2: String GetTemp() {return String();} int main() { String str = GetTemp(); }这里代码中实际只用到了str,但是实际上却调用了一次构造(GetTemp函数中调用String构造生成临时对象)、两次拷贝构造(一次是GetTemp函数调用拷贝构造生成临时对象用于返回、一次是str接收)、三次析构。这里拷贝构造调用了两次深拷贝,但是最后实际使用到的对象却只有str,因此,可以看出,这里有一次深拷贝是多余的。 当堆内存很大时,多余的深拷贝以及其对象的堆内存析构耗时就会变的很可观,那么是否有一种方式,可以让函数中的返回的临时对象空间是否可以不析构,而可以重用呢? 基于上述原因,因此c++11提供了移动构造来解决上述问题。移动构造也是基于右值引用来实现的。 针对上面片段2存在的问题,可以使用移动构造进行解决,下面是移动构造的示例: class HasPtrMem{ public: HasPtrMem():d(new int(3)){ ... } HasPtrMem(const HasPtrMem& h) : d(new int(*h.d)){ } HasPtrMen(HasPtrMem && h) : d(h.d){ //#1 h.d = nullptr; } ... int *d; } HasPtrMen GetTemp() { HatPtrMem h; return h; } int main() { HasPtrMem a = GetTemp(); ... }#1所示的即移动构造函数,它与拷贝构造函数不同的是,它接收的是一个右值引用的参数,即HasPtrMem && h,移动构造函数使用参数h的成员d初始化了本对象的成员d初始化了本对象的成员d(而不是像构造函数一样需要分配内存,然后再将内容一次拷贝到新分配的内存中),而h的成员d随后就被置空。 这里的“偷”堆内存,就是指将对象d指向h.d所指的内存这一条,除此之外,我们还要讲h的d置为空指针,这是因为再移动构造以后,临时对象会被析构,如果不改变h.d的指向的话,那么我们“偷”来的堆内存也被析构掉了。 那么移动构造函数什么时候才会被触发呢?事实上,我们也提供了拷贝构造函数,从外部调用形式来看,拷贝构造及移动构造调用没有分别,那么怎么确保我们调用的是移动构造呢?这就涉及到临时对象的问题。 右值引用与std::movestd::move主要用于将左值强行转换为右值,需要注意的是,被转化的左值生命周期并没有因这种转换而改变。但是在使用std::move时,我们却需要注意:一旦该左值被转换为右值,如果和移动语义结合使用,那么该左值的生命周期就将结束,如果此后还继续使用改左值,那么就会出现严重错误。如下所示: #include using namespace std; classHugeMen{ public: HugeMem(int size) : sz(size > 0 size : 1){ c = new int(sz); } HugeMem(HugeMem && hm) : sz(hm.sz) , c(hm.c){ hm.c = nullptr; } int sz; int *c; } int main() { HugeMem a; HugeMem c(move(a)); cout //#1 m.i = nullptr; } int *i; HugeMem h; } Moveable a(GetTemp());分析上述代码可以发现,GetTemp()临时对象将很快析构,可以避免出现错误。 这里考虑一下,如果#1所在的地方HugeMem不支持移动语义怎么办,这也没多大问题,因为此时会调用其常量左值拷贝函数(上文中已经说明了常量左值是接收右值的),因此也不会有多大问题。基于此,因此我们在编写移动构造函数时应总是将拥有堆内存、文件句柄的资源从左值转换为右值。 移动语义与std::move移动语义与std::move结合时,要格外注意不要误用,下面是一个错误使用的示例: int main() { Moveable a; Moveable c(move(a)); cout c = new int[sz]; } ~HugeMem() {delete[] c;} HugeMem(HugeMem && hm) : sz(hm.sz),c(hm.c){ hm.c = nullptr; } int *c; int sz; }; class Moveable{ public: Moveable() : i(new int(3)),h(1024){} ~Moveable() {delete i;} Moveable(Moveable && m) : i(m.i),h(move(m.h)){ m.i = nullptr; } int *i; HugeMem h; }; Moveable GetTemp(){return Moveable();} int main() { Moveable a(GetTemp()); return 0; }Moveable对象中包含了HugeMem对象,HugeMem类中存在堆内存的分配的成员,如果Moveable传入了一个临时HugeMem对象,那么毫无疑问HugeMem启用移动语义,同时我们同时可以使用std::move将HugeMem的成员转换为右值,从而对该成员也启用移动语义(前提是该成员要存在移动构造函数)。 移动语义注意事项 移动构造函数中要避免使用const右值引用,因为我们最终是要修改右值引用中堆内存指向的。C++11中,实际拷贝/移动构造函数有以下三个版本: T Object(T &) T Object(const T &) T Object(T &&) 一般来说,编译器会隐式的生成一个移动构造函数,不过如果我们自己声明了自定义的拷贝构造函数、拷贝赋值函数、移动赋值函数、析构函数中的一个或多个,那么编译器都不会再生成默认版本。默认版本的移动构造一般也是按位拷贝,这对实现移动语义来说是不够的,通常情况下,如果要实现移动语义,都需要我们自定义移动构造函数。当然,如果类中不包含堆内存,实不实现移动语义都不重要。 考虑到常量的左值引用是万能的,假设我们传入参数类型为右值,但是又没有实现移动语义会怎么样呢?那么就会进入常量拷贝构造函数,这就确保了即使移动构造不成,还可以拷贝。 移动语义与swap移动语义可以实现高效的swap函数,如下: template void swap(T& a,T& b) { T tmp(move(a)); a = move(b); b = move(tmp); }上述代码完全避免了资源的释放与申请,从而完成高效置换。 完美转发所谓完美转发,就是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数。由于拷贝问题的存在,所以完美转发一般不包括值传递。如下: template void IamForwording(T t){ IrunCodeActually(t); }针对上面的例子来说,就是希望:IamForwording传入左值对象,那么IrunCodeActually就获得左值对象;IamForwording传入右值对象,那么IrunCodeActually就获得右值对象。换一句话说,假定IrunCodeActuall有重载左值有右值版本,那么IamForwording传左值就进入IrunCodeActuall左值版本,传右值就进入IrunCodeActuall右值版本。我们当然可以实现两个版本的IamForwording转发函数以对应IrunCodeActuall,但是这样过于繁杂,所以我们更期望实现IamForwording转发函数的模板函数。 虽然貌似很简单,但实际并没有我们想象的那么简单,比如当转发函数为右值,而模板参数为左值,那么我们面临的第一个问题就是,如何确定转发函数提供的实际类型。代码如下: template void IamForwording(T &&t){ IrunCodeActually(t); } T a; IamForwording(a); //a是左值,而转发函数参数又是右值,此时目标函数IrunCodeActually中的是左值还是右值?基于上述原因,所以c++11提供了引用折叠,引用折叠一方面确定了左值右值类型叠加时的类型确定规则,另一方面该规则确保了转发者与接收者的类型一致。 我们可以用两条语句来抽象表示转发者与接收者的参数类型叠加问题: typedef T& TR; TR& v; 如上述代码所示,T、TR的类型可能是左值,也可能右值,那么v最后是左值还是右值呢?针对此,C++11定义了以下的引用折叠规则: 从上表也可以看出,除了第5种(TR 为 T&&,v为TR&,而v实际为A&)类型外,其他都是不需要进行额外转发就能够确保模板参数类型TR和目标函数参数类型v一致。可见引用折叠规则独自无法完成完美转发。因此,C++11在此基础上又提出了std::forward。 分析上表第5种情况可以看出,实际类型为A&,但是我们传入的是T&&,要确保目标函数也收到T&&,那么就只能A&转换为T&&,很明显,这是左右值的转换,我们很自然想起了std::move,但是c++11为了在功能上区别完美转发,所以使用std::forward取代std::move。 完美转发主要用于函数模板中,下面是完美转发示例: #include #include using namespace std; void RunCode(int && m) {} void RunCode(int &m) {} void RunCode(const int && m) {} void RunCode(const int & m) {} template void PerfectForward(T &&t){RunCode(forward(t));} int main() { int a; int b; const int c = 1; const int d = 0; PerfectForward(a); // lvalue ref PerfectForward(move(b)); // rvalue ref PerfectForward(c); // const lvalue ref PerfectForward(move(d)); // const rvalue ref }从上面代码种可以看到,当模板类型为左值时,其进入了目标函数的左值版本,当模板类型为右值时,其进入了目标函数的右值版本,转发函数可以视作不存在,这就是完美转发。 相关代码链接:https://github.com/KevinCoders/MyStudy/blob/master/base/right_value.cpp |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |