右值引用与移动构造函数

不羁岁月 提交于 2020-01-12 19:26:15

右值引用与移动构造函数

左值(lvalue)、右值(rvalue)、xvalue、prvalue、glvalue

定义

  1. c++primer

​ 这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能(区别不是这么简单,这里的只是为了方便记忆)。C++的表达式要么是右值(rvalue),要么是左值(lvalue)。
C++中,一个左值表达式的求值结果是一个对象或者一个函数(然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象)。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。简单归纳:当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。1(《C++ primer 5th edition》)

  1. 官方文档

C++11在官方文档上^2,说明了表达式的值类型。如下所示。
在这里插入图片描述
官方文档(官方文档^23.10节。这样解释:

  • An lvalue (之所以这样称它,因为过去左值可以出现在赋值表达式的左边)表示了一个函数或一个对象。(比如,如果E是一个指针类型,那么*E就是一个左值表达式表示了E指向的对象或者函数。又比如,一个返回类型为左值引用的函数,它的返回结果是一个左值)。
  • An xvalue(一个“到期”值)也是表示一个对象,它通常是在他生命周期的末端(因此,它的资源可以被移动)。一个xvalue是某些涉及右值引用的表达式的结果(比如:一个返回类型为优质引用的函数的返回结果是xvalue)。
  • An glvalue(“广义”左值)是一个lvalue或者xvalue。
  • An rvalue(与左值一样,由于过去右值放在赋值表达式(等式)右边,因此得名),是一种xvalue(一个临时对象或者其子对象),或者是一个不与任何对象有关的值(字面值)。
  • A prvalue(“纯”右值)是一个右值但是却不是xvalue。(比如:一个返回类型为纯右值的函数的返回结果便是纯右值。字面值12,7.3e5,true等都是纯右值)。

​ 每个表达式恰好属于该分类法的类别之一:lvalue,xvalue,或prvalue。 表达式的此属性称为其值类别。 (注意:第5章对每个内置运算符的讨论都指出了它产生的值的类别以及期望的操作数的值类别。例如,内置赋值运算符期望左操作数是lvalue,并且右边的操作数是prvalue,结果是lvalue。 用户定义的运算符是函数,并且他们期望和产生的值的类别取决于它们的参数和返回类型。 )

一些问题

In C++03, an expression is either an rvalue or an lvalue.
In C++11, an expression can be an:
1.rvalue
2.lvalue
3.xvalue
4.glvalue
5.prvalue
Two categories have become five categories.

  • What are these new categories of expressions?
  • How do these new categories relate to the existing rvalue and lvalue categories?
  • Are the rvalue and lvalue categories in C++0x the same as they are in C++03?
  • Why are these new categories needed? Are the WG21 gods just trying to confuse us mere mortals?^3

前三个问题参见官方文档定义!第四个,为什么需要这些新的类型呢?
为了定义和支持移动构造/赋值函数(参考该问题回答^3)。

​ 个人观点:首先想想,为什么不能给右值赋值呢?右值分为xvalue以及prvalue,一个是濒死的对象,一个纯粹的字面值,这两者你给他赋值有何意义,一个即将死亡,即便你还想利用,它也快消亡了,一个你甚至不能找不到其名字,并且与其操作其在内存中的空间,还不如另外自己在写一遍字面值。C++11为何又要右值引用,可想而知,为了救活一个濒死的对象,为了给一个一个纯右值一个名字,这样我们也不用再去开辟空间去保留结果,甚至可以直接将定义一个右值赋值函数,直接使用这个右值的地址,让其成为左值。

左值引用与右值引用

​ 为了支持移动操作,新标准引入一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。通过&&而不是&来获得右值引用。右值引用——只能绑定到一个将要销毁的对象。因此我们可以自由的将一个右值引用的资源“移动”到另一个对象中。2(《C++ primer 5th edition》13.6.1节,471页)
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。如我们所知,对于常规引用,我们不能将其绑定到转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但是不能讲一个右值引用直接绑定到一个左值上。2(《C++ primer 5th edition》13.6.1节,471页)
​ 由于右值引用只能绑定到临时对象,可知:

  • 所引用的对象将要被销毁
  • 该对象没有其他用户
    这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。2(《C++ primer 5th edition》13.6.1节,471页)

测试

#include <iostream>
using namespace std;
int testrr(){
    return 0;
}
int testrr1(){
    int a = 9;
    cout<<"a:"<<(long long)&a<<endl;
    return a;
}

int main(){
    //如下可知每个字面值常量在相同函数中,不同调用,会开辟不同空间去保存它
    int &&rr1 = testrr();
    int &&rr2 = testrr();
    cout<<"test1:"<<endl;
    cout<<"rr1:"<<(long long)&rr1<<endl;
    cout<<"rr2:"<<(long long)&rr2<<endl;
    cout<<"*************"<<endl;
    //以下实验证明,局部变量的地址是固定,但是返回值的地址不是固定的,并且返回值不是局部变量
    int &&rr3 = testrr1();
    cout<<"rr3:"<<(long long)&rr3<<endl;
    cout<<"rr3:"<<rr3<<endl;
    cout<<"---"<<endl;
    int &&rr4 = testrr1();
    cout<<"rr4:"<<(long long)&rr4<<endl;
    cout<<"rr4:"<<rr4<<endl;
    system("pause");
    return 0;
}

在这里插入图片描述

移动构造函数、移动赋值

移动构造函数是什么?与拷贝构造的区别是什么?
可以看看两个例子:
string (const string& str);//拷贝构造
string (string&& str) noexcept;//移动构造
以上为STL中字符串的拷贝构造以及移动构造。移动构造与拷贝构造类型类似,区别在于传入参数一个是左值引用(而且是常左值引用,可知它同样可以传入右值,为了验证传入右值会进入那个函数,见如下代码结果)一个是右值引用。

#include <iostream>
using namespace std;
string rrTest(){
 return "str0";
}
class r1
{
public:
 r1(const string &s0);
 //r1(string &&s0);
 ~r1();
};

r1::r1(const string &s0="s")
{
 cout<<"copy construction"<<endl;
}
// r1::r1(const string &&s0)
// {
//     cout<<"move construction"<<endl;
// }
r1::~r1()
{
}
int main(){
 r1 rr1("1");
 r1 rr2(rrTest());
 system("pause");
 return 0;
}

在这里插入图片描述

#include <iostream>
using namespace std;
string rrTest(){
    return "str0";
}
class r1
{
public:
    r1(const string &s0);
    r1(string &&s0);
    ~r1();
};
r1::r1(const string &s0="s")
{
    cout<<"copy construction"<<endl;
}
r1::r1(string &&s0)
{
    cout<<"move construction"<<endl;
}
r1::~r1()
{
}
int main(){
    r1 rr1("1");
    r1 rr2(rrTest());
    system("pause");
    return 0;
}

进入移动构造函数之后又会是如何对右值进行处理的呢?
在《C++ primer 第五版》这样描述:
移动构造函数与移动赋值运算符类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。除了完成资源移动,移动构造函数话必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向移动的资源——这些源的所有权已经归属新创建的对象。3(《C++ primer 第五版 中文版》13.6.2节,473页)

在这里插入图片描述

为什么要引入移动构造函数?临时变量马上就会被销毁,既然创建都创建了,再销毁肯定是会影响效率的。因此要引入右值引用,不让它销毁了,可想而知,再引入移动构造函数以及移动赋值就是为了利用充分利用临时变量,减少不必要的空间的申请与释放。

这里推荐这篇文章《Move Constructors and Move Assignment Operators (C++)》^6,这里介绍了如何实现移动构造函数以及移动赋值操作符重载。
对比一下拷贝构造函数,就能知道移动构造函数为什么能够带来更少的内存分配以及内存释放操作了。
拷贝构造函数(假设传入参数为右值):
1.根据传入参数带下申请内存。
2.复制传入参数的内容。
3.销毁临时变量(这个操作可能是在构造函数完成之后)。
移动构造函数:
1.将传入参数的数据交由这个对象控制
2.将传入参数数据的地址设为nullptr
3.销毁临时变量
我们能看到实际上,拷贝构造函数会带来两次内存的分配与释放(临时变量以及自身)而移动构造函数(临时变量交由了本身控制,等于自身析构才会释放空间)只有一次。

总结:之前的拷贝构造函数针对如果传入是左值,没有丝毫问题,也不会有多余的分配以及内存释放动作,但是一旦传入的是右值,就需要一个临时变量存储它,它的内存分配与释放完全是可控的。因此就需要区分左值与右值(针对不同的情况做不同的处理),因此就有移动构造函数,既然要用临时变量,那么就把你的控制权给我吧!

Reference


  1. 《C++ primer 第五版 中文版》4.1.1节,121页 ↩︎

  2. 《C++ primer 第五版 中文版》13.6.1节,471页 ↩︎ ↩︎ ↩︎

  3. 《C++ primer 第五版 中文版》13.6.2节,473页 ↩︎

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!