右值引用,移动语义和完美转发


最近在学习学堂在线的linuxcpp课程,其中关于第十章、第十一章提到了几个相关度非常高的重要概念,整理如下。

一、左值与右值

在传统c中,将左值定义为可以出现在赋值号左边或者右边的值,将右值定义为只能出现在赋值号右边的值。换而言之,左值和右值的区别在于能否被修改。
在c++11标准出来后,左值的定义变成了用于标识非临时对象或者非成员函数的表达式,右值的定义变成了用于标识临时对象的表达式或与任何对象无关的值(纯右值),或者用于标识即将失效的表达式(失效值)。
值得注意的是,在c++中,由于一个函数的返回值可以是一个引用,因此一个函数调用本身可以出现在赋值号的左边。
示例如下:

#include<bits/stdc++.h>
using namespace std;


int & test( int & n )
{
    return n;
}


int main()
{
    int a = 10;
    cout << "赋值前:" << a << endl;
    test( a ) = 11;
    cout << "赋值后:" << a << endl;
    return 0;
}

调用结果

赋值前:10
赋值后:11

二、深拷贝

移动语义是相对于拷贝语义而言的。单纯就拷贝而言,存在着浅拷贝和深拷贝两种,浅拷贝将数据块完整的复制一份,效率通常还是不错的。但在实际操作中往往需要深拷贝来进行进行更复杂、更深层次的拷贝构造,以避免发生一些奇怪的错误。

#include<bits/stdc++.h>
using namespace std;


class B
{
public:
    B():_n(0), _p(NULL){}
    explicit B( int n ):_n(n), _p(new int[n]){}
    virtual ~B(){if(_p){delete[] _p,_p = NULL;}}
public:
    void Print();
    int & operator[]( int i ){ return _p[i]; };
    const int & operator[]( int i ) const { return _p[i]; };
private:
    int _n;
    int * _p;
};


void B::Print()
{
    cout << "让我们看看里面有什么?" << endl;
    int n = this->_n;
    for(int i = 0; i < n; i++ )
        cout << this->_p[i] << " ";
    cout << endl;
    return;
}


int main()
{
    B b1(10), b2;
    for( int i = 0; i <10; i++ )
        b1[i] = i;
    b1.Print();
    b2 = b1;
    b2.Print();
    cout << "似乎一切正常." << endl;
    return 0;
}

在这个例子中,我们创建了一个对象b1,并将其拷贝到b2。visual studio中进行调试时会导致debug程序崩溃,在系统中运行只会引起一个错误,经过一段时间后系统状态就恢复了。这是因为在编译器自动创建的浅拷贝中,将_n和_p的值直接复制到了b2中,这使得b2和b1的_p都指向了同一个动态构造的数组。当程序运行结束时,系统将会先析构其中一个对象,这使得另一个对象的_p指针指向了一个不存在的位置从而引发错误。
因此,在对象具有动态构造时我们需要进行深拷贝以避免野指针的出现。

#include<bits/stdc++.h>
using namespace std;


class A
{
public:
    A():_n(0), _p(NULL){}
    explicit A( int && n):_n(n), _p(new int[n]){}
    A( int && n, int * p ):_n(n), _p(p){}
    A( const A & that );
    A & operator=( const A & that );
    virtual ~A(){if(_p){delete[] _p,_p = NULL;}}
public:
    void Print();
private:
    int _n;
    int * _p;
};



A::A( const A & that )
{
    this->_n = that._n;
    _p = new int[_n];
    for( int i = 0; i < _n; i++ )
        _p[i] = that._p[i];
}


A & A::operator=( const A & that )
{
    this->_n = that._n;
    if(_p)
        delete _p;
    _p = new int[_n];
    for( int i = 0; i < _n; i++ )
        _p[i] = that._p[i];
    return *this;
}


void A::Print()
{
    cout << "让我们看看里面有什么?" << endl;
    int n = this->_n;
    for(int i = 0; i < n; i++ )
        cout << this->_p[i] << " ";
    cout << endl;
    return;
}


int main()
{
    A a1(10), a2;
    cout << "接下来a2 = a1:" << endl;
    a2 = a1;
    a1.Print();
    a2.Print();
    cout << "一切正常." << endl;
    return 0;
}

在深拷贝中,我们重新创建了一个新的动态数组并将其赋值。但是显然的,深拷贝语义也存在着问题,那就是在许多时候深拷贝并不是必要的,比如作为返回值的vector对象。vector对象往往十分庞大,而作为返回值的原对象在使用过这一次之后就不再使用了,进行一次深拷贝往往是不必要的。但是进行浅拷贝却会引发野指针的问题,于是我们就有了移动语义。

三、移动语义

移动语义实现所有权的直接移交。他与浅拷贝之间最大的区别在于移动语义完成之后原先的指针将失去对动态数据的所有权,而浅拷贝完成后两个指针都将拥有动态数据的所有权。由于原先的指针失去了对对象的所有权,这个时候即使析构了原先的对象,新对象下的指针也不会空悬。而且移动语义因为没有实际的复制数据,所以也不需要花费大量的时间进行数据复制。

#include<bits/stdc++.h>
using namespace std;


class C
{
public:
    C():_n(0), _p(NULL){}
    explicit C( int n ):_n(n), _p(new int[n]){}
    C( int  n, int * p ):_n(n), _p(p){}
    C( const C & that );
    C & operator=( const C & that );
    C( C && that );
    C & operator=( C && that );
    virtual ~C(){if(_p){delete[] _p,_p = NULL;}}
public:
    void Print();
    int & operator[]( int i ){ return _p[i]; };
    const int & operator[]( int i ) const { return _p[i]; };
    bool checkp(){ return _p; }
private:
    int _n;
    int * _p;
};



C::C( const C & that )
{
    this->_n = that._n;
    _p = new int[_n];
    for( int i = 0; i < _n; i++ )
        _p[i] = that._p[i];
}


C & C::operator=( const C & that )
{
    this->_n = that._n;
    if(_p)
        delete _p;
    _p = new int[_n];
    for( int i = 0; i < _n; i++ )
        _p[i] = that._p[i];
    return *this;
}


C::C( C && that )
{
    this->_n = that._n;
    this->_p = that._p;
    that._p = NULL;
    that._n = 0;
}


C & C::operator=( C && that )
{
    this->_n = that._n;
    this->_p = that._p;
    that._p = NULL;
    that._n = 0;
}


int main()
{
    C c1(10), c2;
    for( int i = 0; i < 10; i++ )
        c1[i] = i;
    cout << "拷贝" << endl;
    c2 = c1;
    if(c1.checkp())
        cout << "c1._p is not NULL." << endl;
    else
        cout << "c1._p is NULL." << endl;
    cout << "移动" << endl;
    c2 = static_cast<C &&>(c1);
    if(c1.checkp())
        cout << "c1._p is not NULL." << endl;
    else
        cout << "c1._p is NULL." << endl;
    return 0;
}

结果输出:

拷贝
c1._p is not NULL.
移动
c1._p is NULL.

四、左值引用和右值引用

由于c++中的引用实际上只是原目标对象的别称,我们在访问引用的时候实际上访问的是原始的目标对象。如果我们不希望一个函数有能力修改原始数据,就不应该使用左值引用,而应该使用深拷贝。但是深拷贝需要付出的代价实在过于高昂,比如在上述例子中我们使用了一种叫做移动语义的东西,就利用了右值引用的一些优点。
在变量被static_cast后,由于该变量被作为右值处理,在这条语句结束后右值将被回收。因此传入时会重新创立一个新的对象,这个对象继承了原来对象的许多特征,包括指针成员所指向的方向。因此就像浅拷贝一样,对右值引用的修改不会影响到原成员,这样就可以让一个参数传递时既可以进行深拷贝,也可以不进行深拷贝,动态成员直接使用原来的。

#include<iostream>
using namespace std;


struct A
{
	A() :_p(new int) { *_p = 0; }
	int *_p;
	int _n = 0;
};


int main()
{
	A a;
	A && b = static_cast<A>(a);
	cout << "Before change. " << endl;
	cout << a._p << endl;
	cout << *a._p << endl;
	cout << &a._p << endl;
	cout << &a._n << endl;
	static_cast<A>(a);
	a._p = new int;
	cout << "after static_cast. " << endl;
	cout << a._p << endl;
	cout << *a._p << endl;
	cout << &a._p << endl;
	cout << &a._n << endl;
	return 0;
}

运行结果:

Before change.
001504C0
0
00EFF858
00EFF85C
after static_cast.
00155B40
-842150451
00EFF858
00EFF85C

其中的-842150451是来自系统的赋值,也就是没有初始化的动态整数应有的数据(在VS中)。如果这个右值被传给了另一个函数作为参数,就不会被直接回收,这样就和浅拷贝相似了。
但是实际上,右值引用还存在两个问题。首先,右值引用的实现完全可以通过其他的方法来实现,比如使用const引用传参,参数在函数内同样不会被修改,这样一来使用右值引用就显得很多余。其次在函数内部,右值引用的变量实际上还是作为一个左值来对待的(允许赋值),因此实际上对动态对象的修改却可以传到函数外,这和右值引用的语义实际上并没有符合得很好,让人很困惑右值引用真正的含义。

#include<iostream>
using namespace std;


struct A
{
	A() :_p(new int) { *_p = 0; }
	int *_p;
	int _n = 0;
};


void changel(A & a)
{
	*a._p = 1;
	cout << "in changel. " << endl;
	cout << a._p << endl;
	cout << *a._p << endl;
	cout << &a._p << endl;
	cout << &a._n << endl;
	cout << a._n << endl;
}


void changer(A && a)
{
	a._n = 9;
	*a._p = 2;
	cout << "in changer. " << endl;
	cout << a._p << endl;
	cout << *a._p << endl;
	cout << &a._p << endl;
	cout << &a._n << endl;
	cout << a._n << endl;
}


int main()
{
	A a;
	A && b = static_cast<A>(a);
	cout << "Before change. " << endl;
	cout << a._p << endl;
	cout << *a._p << endl;
	cout << &a._p << endl;
	cout << &a._n << endl;
	cout << a._n << endl;
	changel(a);
	cout << "after changel. " << endl;
	cout << a._p << endl;
	cout << *a._p << endl;
	cout << &a._p << endl;
	cout << &a._n << endl;
	cout << a._n << endl;
	changer(static_cast<A>(a));
	cout << "after changer. " << endl;
	cout << a._p << endl;
	cout << *a._p << endl;
	cout << &a._p << endl;
	cout << &a._n << endl;
	cout << a._n << endl;
	return 0;
}

运行结果:

Before change.
000E04C0
0
00CFFEA0
00CFFEA4
0
in changel.
000E04C0
1
00CFFEA0
00CFFEA4
0
after changel.
000E04C0
1
00CFFEA0
00CFFEA4
0
in changer.
000E04C0
2
00CFFDA4
00CFFDA8
9
after changer.
000E04C0
2
00CFFEA0
00CFFEA4
0

在这个例子中我们看到,右值引用传到函数内后,两个函数成员的地址均不是原来的地址,但是成员的值(_n的值和_p指针所指向的位置)没有发生变化。也就是说右值引用实现了类似于浅拷贝的效果,并且这个浅拷贝可以和深拷贝同时存在。除此之外我们还可以看到即使是通过右值引用将参数传进了函数,对于成员的修改虽然不会传外部,但是成员实际上依旧可以被赋值,动态成员依旧可以被修改。

五、完美转发

参考:C++ 11完美转发(原作地址已经失效)
从上面可以看出,右值引用的意义在于右值引用传参实际上进行了拷贝,这使得在函数内对成员的修改并不在意原对象上直接进行。同时右值引用不进行动态成员的赋值,因此不需要耗费大量的时间去拷贝庞大的动态构造的成员。但是这些并不是没有替代方案,也存在一些问题。右值引用真正无法替代的地方在于参数的转发。
由于一个右值参数不能传左值进去,一个非常量左值不能传常量左值进去。所以如果我们的函数有时候有时候需要传左值,有的时候还需要传变量进去的时候,对于一个拥有n个参数的函数,就需要重载 4 n 4^n 4n个函数,这显然是不现实的。
那么有没有办法只需要实现少量的函数就可以进行函数的转发呢?这当然是可以的。比如说,我们可以使用const_cast,但是这样我们将会改变参数的原始语义。在比如说可以修改参数推导规则进行转发,但是这样本身会对c++体系造成严重的破坏。这在程序界一直是一个难题。但在c++11之后,使用右值引用和参数模板就可以实现参数的完美转发。
首先介绍一下std::forward这个函数模板,他在参数模板为右值的时候将对象转换成右值。于是我们有了这样一个函数对象。

#include<string>
using namespace std;


class A
{
public:
    template<typename T1, typename T2> A( T1 && s, T2 && t)
    :_s(forward)<T1>(s)), _t(forward<T2>(t)){}
private:
    string _s, _t;
}

这样当我们编译的时候将会自动的创建对应的构造函数。传递的是右值引用,就会构造右值引用的函数版本,传递的是左值引用,就会构造左值引用的版本。

部分代码来自学堂在线linuxcpp课程代码。

猜你喜欢

转载自blog.csdn.net/SDDX_CDY/article/details/87713436