学习笔记:右值引用


前言

最近在阅读STL源码时遇到了“右值引用”的概念,查阅了一些资料后才明白是什么意思,特此记录一下,方便以后回头再复习一下。

“引用”的概念

“引用”就是别名,也就是对一个对象另起一个名字,相当与平常我们的小名。在我们学习C++时,接触到了引用的概念。其实,之前我们学习C++时的引用是“左值引用”,在C++11中才开始提出了“右值引用”的概念。从此,“引用”分为两类:左值引用、右值引用。

在介绍“右值引用”前,先说一下“左值引用”。

1.左值引用

左值引用的基本语法:

Type &引用名 = 左值表达式;

int a = 10;
int &b = a;//相当与 小a 的小名叫 小b

通过上面的定义,当我们在使用变量b的时候就相当于在使用变量a。
例如:

#include <iostream>
using namespace std;
int main()
{
 int a = 10;
 int &b= a;//相当于 a 的小名叫 b
 //通过上面的定义,当我们在使用变量b的时候就相当与在使用变量a
 cout << b++ << "  " << (void*)&b << endl;
 cout << a   << "  " << (void*)&a << endl;
 return 0;
}

运行后:

输出

引用的基本规则:

  1. 声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象。
  2. 对引用的一切操作,就相当于对原对象的操作。

2.右值引用

在只有“左值引用”的时候,理解起引用的概念还不算困难,当遇到“右值引用”的概念后,发现自己根本就不知道什么是引用了….泪奔ing~~o(>_<)o ~~
为了更好的理解“右值引用”,先介绍下什么是右值和左值:

在C语言中,我们常常会提起左值(lvalue)、右值(rvalue),并没有严格的定义,一个典型的判别方法就是在表达式中,出现在等号右边的为“右值”,左边的为“左值”。例如
a = b + c;
在这个表达式中,a就是一个左值,而b+c为右值。

而在C++则有另外一个广泛认同的说法,那就是可以取地址、有名字的就是左值,其余的就是右值。由于b+c不能取地址,所以它是一个右值。再比如:非引用返回的函数返回的临时变量、1+3、’c’、2、true都是右值。

2.1右值引用的功能

为了防止消化不良,下面,通过一个个例子来慢慢体会一下右值引用:

#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<objfoo()
{
    vector<objc;
    c.push_back(obj());

    cout << "---- exit foo ----" << endl;
    return c;
}

int main()
{
    vector<objk;
    k = foo();
}

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

tianfang g++ main.cpp
tianfang a.out
>create obj 
>copy create obj 
---- exit foo ----
>copy create obj 
tianfang >

可以看到,对obj对象执行了两次构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机:

  • foo函数第二行,调用push_back的时候,会在vector里建立一个obj的副本
  • main函数第二行,执行复制函数的时候,会把foo()返回的对象全部复制过来,再次执行一次拷贝构造

由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。

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

可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?

在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下:

  • foo()函数返回一个临时对象(这里用~tmp来标识它)
  • 执行vector的 ‘=’ 函数,将对象k中的现有成员删除,将~tmp的成员复制到k中来
  • 删除临时对象~tmp

在C++11的版本中,执行过程如下:

  • foo()函数返回一个临时对象(这里用~tmp来标识它)
  • 执行vector的 ‘=’ 函数,将对象k中的成员~tmp的成员互换,此时k中的成员就被替换成了~tmp中的成员。
  • 删除临时对象~tmp(此时就删除了以前的k中的成员)

关键的过程就是第2步,它不是复制而是交换,从而避免的成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。

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

2.2通过交换减少数据的拷贝

为了演示如何在我们的代码中也获取这个性能提升,首先我先写了一个山寨的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<objfoo()
{
    container<objc;
    c.push_back(obj());

    cout << "---- exit foo ----" << endl;
    return c;
}

int main()
{
    container<objk ;
    k = foo();    
}

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

tianfang make
g++ -std=c++11 main.cpp
tianfang a.out
>create obj 
>copy create obj 
---- exit foo ----
>copy create obj 
tianfang >

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

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<objk, k2;
    k = foo();    

    //预期结果是复制,但执行了交换
    k2 = k;
}

gcc的告警是有道理的:如果 ‘=’ 函数实现的是复制功能,虽然效率低点,但保证了功能正确,但如果实现的是交换的功能,则不能保证功能一定正确。只有当 ‘=’ 函数右边的对象为一个临时变量的时候,由于临时变量会马上被删除掉,此时的交换和复制的效果是一样的。其实VC也应该把这个告警加上才合适。

PS:对临时变量定义和来源不清楚的朋友可以参考一下这篇文章

现在的问题是:我们无法在赋值函数里区分传入的是一个临时对象还是非临时对象,因此只能执行复制操作。为了解决这一问题,c++中引入了一个新的赋值函数的重载形式:

container& operator = (container&& other)

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

入参不是const型,因此它是可以更改入参的值的,从而实现交换操作
入参前面有两个&号,这个是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<bigobjlist;
    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后执行了交换操作,如果再次使用它会出现非预期的结果,只有能确定该变量不会再次被使用才能执行这种转换。

总结

  1. 右值引用的确带来了性能提升的可能,也带来了完美转发。但性能的提升需要额外的编码,用来实现转移和交换行为。
  2. 左值和右值的概念,并不是指等号的左边还是右边,而是指该表达式代表的对象是否是持久的,持久的就是左值,反之是右值。我们也可以用是否具名来判断,具名的是左值,反之是右值(非具名左值引用除外)。
  3. std::move()函数用于语义上的转移,将左值转成右值,而不是实质上数据的转移,实质性的转移需要每个类单独编码实现。

本文参考

C++拾遗–引用(左值引用、右值引用)

C++ 11 中的右值引用

探索C++0x: 3. 右值引用(rvalue reference)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值