1.3值类别(value category)-1.3.1-理解左值和右值

由于表达式产生的中间结果会导致多余的拷贝,因而在c++11中引入了移动语义来解决这个问题,同时对值类别的右值和左值进行重新定义。需要注意的是,值类别指的是表达式结果的类别,并不是指对象,变量或者类型的类别

考虑如下代码,应采用哪个foo函数的版本

void foo(int&);//#1
void foo(int&&);//#2

int && value =5;
foo(value);

应采用第一个版本的foo,虽然这里的变量value的定义是一个右值引用类型,然而foo(value)中的value却是一个左值表达式,而不是由定义value时的类型来决定其值类别。通俗的说,可以理解成表达式能取地址,作为左值表达式;否则,为右值表达式(包括临时对象)

这里的value很明显可以取地址&value,因此表达式value是个左值,其类型为右值引用,绑定了5,因为5为字面量不能取地址,所以‘5’为右值

根据引用与常量性进行结合,可以形成以下几种情况:

  • 左值引用value&,只能绑定左值表达式,例如上述第一个版本的foo函数形参
  • 右值引用value&&,只能绑定右值表达式,例如上述第二个版本的foo函数形参
  • 左值常引用const value&,可以绑定左,右值表达式,只读
  • 右值常引用const value&&,只能绑定常量右值表达式,用的较少

那么如何将value作为右值表达式调用第二个版本的foo函数呢?

通过foo(5)将比配第二个版本,因为‘5’是个右值表达式,能够被foo形参的右值引用绑定,但是这明显不是我们想要的方式

答案是通过static_cast<int&&>(value)做到,即强制转换,也是std::move的实现方法

对于表达式static_cast<int&&>(value)来说,它是右值表达式还是左值表达式呢?它可以被右值引用绑定,且具备左值的运行时多态性质,对于这种既具有左值的特征,同时又能初始化右值引用的情况,在c++11中将其归为将亡值

因此c++将原来的右值称为纯右值(prvalue),把将亡值(xvalue)与纯右值(prvalue)统称为右值(rvalue);

而把左值(lvalue)与将亡值(xvalue)称为泛左值(gvalue)

当程序引入了右值引用后,就可以表达移动语义,比较常见的就是拷贝语义,拷贝语义与移动语义都是将原值赋予目的值,对于目的值来说其内容都是原值的内容,唯一区别就是拷贝语义不会清理(修改)原值的内容,而移动语义可能会1

在c++11之前由于没有右值引用,实现部分移动语义时常常要使用swap模式来表达。在这之后若一个类实现了移动构造函数,那么当该对象为临时2的或者该对象被手动std::move3后,将触发移动构造

  1. 移动语义是否会修改原值的内容,取决于实现。基础数据类型的移动语义就不会修改原来的值,而用户定义的类型可以自行决定移动语义的行为 ↩︎
  2. 最常见的移动语义就是函数的返回值,将被自动地触发移动语义 ↩︎
  3. vector(vector&& other) noexcept;
    std::vector <int> x{1,2,3,4};
    std::vector<int> y(std::move(x));//c++11之前使用swap(y,x);表达移动语义
    assert(x.empty());//1,程序并没有终止
    assert(y.size()==4);//1,程序并没有终止
    ↩︎

vector的移动构造函数实现标准库代码:

vector(vector&& rhs)noexcept{
begin_=rhs.begin_;
end_=rhs.end_;
end_cap_=rhs.end_cap_;
rhs,begin_=rhs.end_=rhs.end_cap_=nullptr;
}

简而言之,移动构造时新的vector对象接管了被移动对象的堆指针,并在最后清空(修改)了被移动对象的堆指针,所以从结果上看就是数据被移动了。std::move在这里显式地表达了移动语义,因为他将左值进行移动,如果没有这个std::move的行为,就是传统的拷贝行为。

传统意义上的理解是,在赋值操作符的左边为左值,右边为右值,这种理解在现代c++上是错误的

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部