C++11中的右值引用

来源:互联网 发布:淘宝根据图片搜索 编辑:程序博客网 时间:2024/06/05 07:31

左值和右值

  既然要讨论右值引用,那么首先得分清左值和右值。

  左值与右值这两概念是从 c 中传承而来的,在 c 中,左值指的是既能够出现在等号左边也能出现在等号右边的变量(或表达式),右值指的则是只能出现在等号右边的变量(或表达式)。

int a;int b;a = 3;b = 4;a = b;b = a;// 以下写法不合法。3 = a;a+b = 4;

  在 c 语言中,通常来说有名字的变量就是左值(如上面例子中的 a, b),而由运算操作(加减乘除,函数调用返回值等)所产生的中间结果(没有名字)就是右值,如上的 3 + 4a + b 等。我们暂且可以认为:左值就是在程序中能够寻值的东西,右值就是没法取到它的地址的东西(不完全准确),但如上概念到了 c++ 中,就变得稍有不同。

  具体来说,在 c++ 中,每一个表达式都会产生一个左值,或者右值,相应的,该表达式也就被称作“左值表达式”, “右值表达式”。对于内置的基本数据类型来说(primitive types),左值右值的概念和 c 没有太多不同,不同的地方在于自定义的类型,而且这种不同比较容易让人混淆:
  1)对于内置的类型,右值是不可被修改的(non-modifiable),也不可被 const, volatile 所修饰(cv-qualitification ignored)
  2)对于自定义的类型(user-defined types),右值却允许通过它的成员函数进行修改。

  值得注意的是,对于前面提到的右值的两个特性:
  1) 允许调用成员函数。
  2) 只能被 const reference 指向。
  也就是说const A& a = GetA()是正确的,但是A& a = GetA()就是错误的。
  
  左值的声明符号为&,右值的声明符号为&&。在C++中,临时对象不能作为左值,但是可以作为常量引用const &。
  

#include<iostream>void print_lvalue(int& i)//左值{    std::cout << "Lvalue:" << i << std::endl;}void print_rvalue(int&& i)//右值{    std::cout << "Rvalue:" << i << std::endl;}int main(){    int i = 0;    print_lvalue(i);    print_rvalue(1);    //print_lvalue(1)会出错    //print_lvalue(const int& i)可以使用print_lvalue(1)    return 0;}

C++中的临时变量

  C++中真正的临时对象是看不见的,它们不出现在你的源代码中,临时对象的产生在如下几个时刻:

1)用构造函数作为隐式类型转换函数时,会创建临时对象。

class Integer{public:    Integer(int i):m_val(i) {}    ~Integer() {}private:    int   m_val;};void Calculate(Integer itgr){    // do something}

那么语句

int  i = 10;Calculate(i);

会产生一个临时对象,作为实参传递到Calculate 函数中。

2) 建立一个没有命名的非堆(non-heap)对象,也就是无名对象时,会产生临时对象。

Integer& iref = Integer(5);   //用无名临时对象初始化一个引用,等价于Integer iref(5);Integer  itgr = Integer(5);  //用一个无名临时对象拷贝构造另一个对象

  按理说,C++应先构造一个无名的临时对象,再用它来拷贝构造itgr,由于该临时对象拷贝构造 itgr 后,就失去了任何作用,所以对于这种类型(只起拷贝构造另一个对象的作用)的临时对象,c++特别将其看做: Integer itgr(5); 即直接以相同参数构造目标对象,省略了创建临时对象这一步。
  

Calculate( Integer(5) );   //无名临时对象作为实参传递给形参,函数调//用表达式结束后,临时对象生命期结束,被析构.

3) 函数返回一个对象值时,会产生临时对象,函数中的返回值会以值拷贝的形式拷贝到被调函数栈中的一个临时对象。

Integer Func(){    Integer itgr;    return itgr;}void main(){    Integer in;    in = Func();}

  表达式 Func() 处创建了一个临时对象,用来存储Func() 函数中返回的对象,临时对象由 Func() 中返回的 itgr 对象拷贝构造(值传递),临时对象赋值给 in后,赋值表达式结束,临时对象被析构。见下图:
  这里写图片描述
  看看如下语句:

Integer& iRef = Func();

  该语句用一个临时对象去初始化iRef 引用,一旦该表达式执行结束,临时对象的生命周期结束,便被结束,iRef引用的尸体已经不存在,接下来任何对 iRef 的操作都是错误的。

c++11中的右值引用

右值引用的功能

  首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能:
  

#include <iostream>#include <vector>using namespace std;class obj{public :    obj() { cout << ">> create obj " << endl; }    obj(const obj& other) { cout << ">> copy create obj " << endl; }};vector<obj> foo(){    vector<obj> c;    c.push_back(obj());    cout << "---- exit foo ----" << endl;    return c;}int main(){    vector<obj> k;    k = foo();}

  首先我们编译一下这个函数,运行结果如下:
  

fanxin > g++ main.cppfanxin> a.out>> create obj >> copy create obj ---- exit foo ---->> copy create obj fanxin >

可以看到,对obj对象执行了两次构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机:
  1.foo函数第二行,调用push_back的时候,会在vector里建立一个obj的副本 。
  2.main函数第二行,执行复制函数的时候,会把foo()返回的对象全部复制过来,再次执行一次拷贝构造 。
  由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。
  

fanxin> g++ -std=c++11 main.cppfanxin > a.out>> create obj >> copy create obj ---- exit foo ----fanxin>

  可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?
在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下:
  1.foo()函数返回一个临时对象(这里用~tmp来标识它)
  2.执行vector的 ‘=’ 函数,将对象k中的现有成员删除,将~tmp的成员复制到k中来
  3.删除临时对象~tmp

在C++11的版本中,执行过程如下:
  1.foo()函数返回一个临时对象(这里用~tmp来标识它)
  2.执行vector的 ‘=’ 函数,将对象k中的成员~tmp的成员互换,此时k中的成员就被替换成了~tmp中的成员。
  3.删除临时对象~tmp(此时就删除了以前的k中的成员)
  
  关键的过程就是第2步,它不是复制而是交换,从而避免的成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。

  但是,这份免费的午餐也不是无条件就可以获取的,带上-std=c++11编译时,如果使用STL代码可以享用这份午餐,但如果使用我们以前的老代码发现还是和以前的功能是一样的,那么,如何让我们以前的代码也能得到这个效率的提升呢?

通过交换减少数据的拷贝

  为了演示如何在我们的代码中也获取这个性能提升,首先我先写了一个山寨的vector(这个vector只能容纳一个元素):
  

#include <iostream>#include <vector>using namespace std;class obj{public :    obj() { cout << ">> create obj " << endl; }    obj(const obj& other) { cout << ">> copy create obj " << endl; }};template <class T>class container{public:    T* value;public:    container() : value(NULL) {};    ~container() { delete value; }     container(const container& other)    {        value = new T(*other.value);    }    const container& operator = (const container& other)    {        delete value;        value = new T(*other.value);        return *this;    }    void push_back(const T& item)    {        delete value;        value = new T(item);    }};container<obj> foo(){    container<obj> c;    c.push_back(obj());    cout << "---- exit foo ----" << endl;    return c;}int main(){    container<obj> k ;    k = foo();    }

  这个vector只能容纳一个元素,但并不妨碍我们的演示,其功能和前面的例子是一样的,运行这段代码,结果如下:
  

fanxin> makeg++ -std=c++11 main.cppfanxin> a.out>> create obj >> copy create obj ---- exit foo ---->> copy create obj fanxin>

  如前所述,仍然有两次拷贝构造。其实前面已经说过交换实现减少拷贝构造的原理,那么,我们可以通过修改 ‘=’ 函数来手动实现这一过程。
  

const container& operator = (container& other){    T* tmp = value;    value = other.value;    other.value = tmp;    return *this;}

  在VC中运行这段代码,发现运行结果和预期一致,
  

>> create obj >> copy create obj ---- exit foo ----

  但是,gcc中却无法通过编译,原因很简单:gcc期望的赋值函数的参数是const型的,而这里为了交换成员,而不能使用const型。

  那么,虽然gcc中不能生效,是否可以说在vc中就可以以这种形式获取性能提升呢?答案是否定的。虽然在这段代码中这么写没有问题,但赋值函数本身是期望复制功能的,而不是交换。例如,修改后下面的运行结果就不对了。
  

int main(){    container<obj> k, k2;    k = foo();        //预期结果是复制,但执行了交换    k2 = k;}

  gcc的告警是有道理的:如果 '=' 函数实现的是复制功能,虽然效率低点,但保证了功能正确,但如果实现的是交换的功能,则不能保证功能一定正确。只有当 '=' 函数右边的对象为一个临时变量的时候,由于临时变量会马上被删除掉,此时的交换和复制的效果是一样的。其实VC也应该把这个告警加上才合适。
  
  现在的问题是:我们无法在赋值函数里区分传入的是一个临时对象还是非临时对象,因此只能执行复制操作。为了解决这一问题,c++中引入了一个新的赋值函数的重载形式:

container& operator = (container&& other) 

这个赋值函数通常称为移动赋值函数,和老版本的相比,它有两点区别:

  1.入参不是const型,因此它是可以更改入参的值的,从而实现交换操作
  2.入参前面有两个&号,这个是C++11引入的新语法,称为右值引用,它的使用方式和普通引用是一样的,唯一的区别是可以指向临时变量。

  现在,我们就有两个版本的赋值函数了,C++11在语法级别也做了适应:

  • 如果入参是临时变量,则执行移动赋值函数,如果没有定义移动赋值函数,则执行复制赋值函数(以保证老版本代码能编译通过)
  • 如果入参不是临时变量,则执行普通的复制赋值函数

现在,我们实现一下山寨版的移动赋值函数:

container& operator = (container&& other){    delete value;    value = other.value;    other.value = NULL;    return *this; }

  运行后结果就和我们期望的那样,避免了成员的第二次的拷贝构造。

  和移动赋值函数相应的,也有一个一个移动构造函数,也最好实现以下:

container (container&& other){    value = other.value;    other.value = NULL;}

  我们也可以实现自己的右值引用版的重载函数,这里就不多介绍了。

  完善的版本请看MSDN文章:如何编写一个移动构造函数,其相应的对右值引用的介绍文章Rvalue引用声明:&&也非常值得一读。

通过std::move函数显式使用交换

  
  首先看一下这段代码:
  

class bigobj{public :    bigobj() { cout << ">> create obj " << endl; }    bigobj(const bigobj& other) { cout << ">> copy create obj " << endl; }    bigobj(bigobj&& other) { cout << ">> move create obj " << endl; }};int main(){    list<bigobj> list;    for(int i = 0; i < 3; i++)    {        bigobj obj;        list.push_back(obj);    }} 

  运行的时候就会发现:虽然我们定义了移动构造函数,但是它仍然会执行拷贝构造函数。这是因为编译器并不认为obj是临时变量。简单的说,我们能够看到的命名变量都不是临时变量。

  虽然obj对象不是语言级别的临时变量,但是从功能上来看,它就是一个临时变量,是可以使用移动构造函数来消除拷贝带来的性能损失的。为了解决这一问题,C++提供了一个move函数来把obj变量强制转换为右值引用,这样就可以使用移动构造函数了。
  

for(int i = 0; i < 3; i++){    bigobj obj;    list.push_back(std::move(obj));} 

  不过,需要注意的是,和系统识别的临时变量而自动使用右值引用不同,这种强制转换是有一定的风险的,由于在push_back后执行了交换操作,如果再次使用它会出现非预期的结果,只有能确定该变量不会再次被使用才能执行这种转换。

0 0
原创粉丝点击