左值和右值

什么是右值引用

右值引用是C++11中引入的新特性,它实现了转移语义和精确传递。他的两个主要目的有两个方面:

1.消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。

2.能够更简洁明确地定义泛型函数。

左值和右值的概念

左值:能对表达式取地址、或具名对象、变量。一般表达式结束后依然存在的持久对象。

右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象

左值和右值之间的转换

一般上来讲,对象之间的运算,对象是以右值的形式参与的。比如二元运算符+两边的参数以右值传入,返回结果也是右值。

左值引用

C++中可以使用&定义引用,如果一个左值同时是引用,就称为“左值引用”,

1
2
string s;
string &a=s;

非const左值引用不能使用右值对其赋值;

1
string &r=string();

如果假设可以的话,就会遇到一个问题:如何修改右值的值?因为引用是可以后续被赋值的。根据上面的定义,右值连可被获取的内存地址都没有,也就谈不上对其进行赋值。

但是const左值引用不一样,因为常量不能被修改,也就不存在上述问题。

1
const string &s = string();

右值引用(C++11)

右值引用及其相关的move语义是C++11新引入的最强大的特性之一。

他主要用来解决C++98/03中的两个问题,第一个问题是临时对象非必要的常规的拷贝操作,第二个问题是在模板函数中如何按照参数的实际类型进行转发。

4行代码的故事

第一行代码的故事

1
int i = getval();

这行代码会产生两种类型的值,一种是函数返回的临时对象,一种是左值i。这个临时对象在表达式结束后就销毁了,而i在表达式之后仍然存在,这个临时值就是右值。

1
int i = 0;

i是左值,0是字面量,就是右值。

在C++11中所有的值都属于左值,将亡值,纯右值三者之一。

非引用返回的临时对象、运算表达式产生的临时变量、原始字面量和lambda表达式都是纯右值。

将要被移动的对象,T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值都属于将亡值。

第二行代码的故事

1
T&& k = getval();

这行代码就是右值引用,getval()产生的临时值不会像第一行代码一样,在表达式结束之后就销毁,而是会被续命,他的生命周期将会通过右值引用得以延续,和变量k的声明周期一样长。

右值引用的第一个特点

通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。

第三行代码的故事

1
T(T&& a):m_val(val){a.m_val=nullptr;}

这行代码实际上来自于一个类的构造函数,构造函数的一个参数是一个右值引用。

一个带有堆内存的类,必须提供一个深拷贝构造函数,因为默认的拷贝构造函数是浅拷贝,会发生“指针悬挂”的问题。如果不提供深拷贝构造函数,上面的测试代码将会发生错误,内部的m_val会被删除两次,一次是临时右值析构的时候删除一次,第二次外面构造的a对象释放时删除一次,而这两个对象的m_ptr是同一个指针,这就是所谓的指针悬挂问题。提供深拷贝构造函数虽然可以保证正确,但是在有些时候会造成额外的性能损耗。

实际上为了解决这个问题,C++11提供了std::move方法将左值转换成右值,为了方面应用移动语义。move是将对象资源的所有权从一个对象转移到另一个对象,只有转移,没有内存的拷贝。move实际上他并不能移动任何东西,他唯一的功能是将一个左值强制转换为一个右值引用。如果是一些基本类型比如int和char[10]定长数组等类型,使用move仍然会发生拷贝(因为没有对应的移动构造函数)。

第4行代码的故事

1
2
3
4
template <typename T>void f(T&& val)
{
foo(std::forward<T>(val));
}

C++11之前调用模板函数时,存在一个比较头疼的问题,如何正确的传递参数。

C++11引入了完美转发,在函数模板中,完全依照模板的参数的类型(既保持参数的左右值特征),将参数传递给函数模板中调用的另一个函数。C++11的std::forward正是做这个事情的,他会按照参数的实际类型进行转发。