C++11新特性--右值引用与移动语义

引子-深拷贝和浅拷贝

    在cpp11之前,我们定义一个类如果类中有指针成员,并且其指向一块堆内存,那么往往本类要负责这个指针指向内存的分配和销毁,不然会产生令人讨厌的内存泄露问题。但如果这个类的对象之间进行复制就会涉及到数据的拷贝问题,如果不加处理,使用编译器默认生成的拷贝构造函数,那么默认的行为就是按bit进行memory copy,这就是所谓的浅拷贝。浅拷贝的后果是两个对象持有两个不同的成员指针,两个指针指向同样的堆内存,如果其中一个销毁了内存,另一个再用,就会产生无法预知的错误,导致程序错误。所以针对这种情况我们往往要提供自定义版本的拷贝构造函数,来确保指针指向的内存也有一份拷贝。可以看一下代码示例:

class PtrMemTest
{
public:
    int* p;
    PtrMemTest(int value)
    {
        p = new int(value);
    }
    ~PtrMemTest()
    {
        if (p)
        {
            delete p;
            p = nullptr;
        }
    }
    PtrMemTest(const PtrMemTest& obj)
    {
        p = new int(*obj.p);
    }
};

    如果我们不定义拷贝构造函数,那么就会引起浅拷贝的问题,继而可能引起严重的程序错误。提供了拷贝构造函数之后,我们保证了程序的正确性,但是内存的频繁拷贝是一种性能开销,如果允许我们在一些情况下只是把指针的指向的内存所有权转移应该如何呢?比如下面的代码

PtrMemTest obj2(PtrMemTest(12));

    这里只是用一个临时对象去初始化另一个对象,会产生两次堆内存的申请,其中一次实际完全没有必要,因为是个临时对象,完成了第二个对象的初始化之后就马上销毁了。这个例子比较极端,在平时程序中不常见,那么用一个函数返回值去初始化一个对象的场景就极为常见了。如代码

PtrMemTest GetMemObj(int value)
{
    return PtrMemTest(value);
}
void testMem()
{
    PtrMemTest obj3(GetMemObj(13));
}

    这种情况下跟上面情况类似,函数的返回值作为一个临时对象,去初始化obj3这个对象,完成使命后就自行销毁,产生了不必要或者说可以优化的内存拷贝。这种情况实际上会产生一次构造和两次拷贝构造(实际编译器会对这种情况做优化,从而不会产生额外的内存释放和销毁),一次是PtrMemTest(value)产生的构造,一次是函数GetMemObj中产生的一个临时对象作为函数的返回值,而它用刚才的对象进行构造,从而产生了一次拷贝构造,最后一次拷贝构造发生在构造obj3的时候。那么如果可以有效利用临时对象,把它们的内存“偷过来“就可以减少一次有可能成本十分昂贵的拷贝构造。在cpp11中,有了对这些场景的一个解决方案--右值引用。

什么是右值

    在c中我们可以近似的认为赋值号左边的称为左值(lvalue)右边的称为右值(rvalue)。如

int a = 3;
int b = a + 5;

    其中a和b就是左值,3和a+5都是右值。
    还有一个比较被广泛认同的定义,可以被合法取地址的值称为左值,反之称为右值。如&a, &b都是合法表达式,所以他们都是左值;但是&3,&(a+5)都是不合法的表达式,所以他们不能取地址,进而它们即是右值。那么容易看出来函数的返回值是一个临时值,无法被取地址,所以是个右值。

右值引用

    cpp11中右值引用就是对一个右值进行引用的类型。由于右值通常不具有名字,我们也只能通过引用的方式绑定它。如

int&& a = ReturnIntRvalue();
int&& b = 12;

int& c = ReturnIntRvalue();//compile fail
const int& d = ReturnIntRvalue();

    上面代码中前两句都是右值引用绑定右值的例子,右值引用只能绑定到右值上,否则会编译失败。值得一提的是,第三句无法编译通过,由于cpp不允许左值引用绑定到右值上,但是第四句却能编译通过,原因是const T&类型是万能类型,可以绑定到任何类型,左值,常量左值,右值。不过这里相对于右值引用,它是只读的,而右值引用是可以改变所引用的右值的值的。
再看一下代码

class PtrMemTest
{
public:
    int* p;
    PtrMemTest(int value)
    {
        std::cout<<"PtrMemTest"<<std::endl;
        p = new int(value);
    }
    ~PtrMemTest()
    {
        std::cout<<"~PtrMemTest"<<std::endl;
        if (p)
        {
            delete p;
            p = nullptr;
        }
    }
    PtrMemTest(const PtrMemTest& obj)
    {
        std::cout<<"PtrMemTest copy"<<std::endl;
        p = new int(*obj.p);
    }

    PtrMemTest(PtrMemTest&& obj)
    {
        std::cout<<"PtrMemTest move"<<std::endl;
        p = obj.p;
        obj.p = nullptr;
    }
};
    static void execute()
    {
        PtrMemTest obj1(111);
        PtrMemTest obj2(std::move(obj1));
    }

    这里对前面的代码做了增加,增加了一个cpp11中新增的移动构造函数,它的作用正是前面我们希望的将一个右值(临时对象)的内容“偷”过来,用最小的代价来构造新的对象。move这个库函数,用来强制将一个值转换成右值。所以这里运行的结果是

PtrMemTest
PtrMemTest move
~PtrMemTest
~PtrMemTest

    obj2将自己的成员指针指向了obj1中开辟的内存,obj1此后变成了一个空对象。这里为了规避编译器的优化,所以写成了这种方式,这种方式是有风险的,因为在obj2构造后,程序就不应该再使用obj1了,否则可能出现问题。更合理的用法是

 PtrMemTest obj2(GetPtrMemObj());

    这样函数产生的临时返回值就会被“移动”到obj2中,从而减少了内存的分配和销毁。

移动语义

    前面的例子所展示即为移动语义。标准库中提供了一个有用的函数std::move来强制将左值转换成右值,正如刚才例子中所展示的。有了这个标准库函数,我们可以更加灵活的按自己的需求来将左值转换成右值。比如继承一个有移动构造的基类,子类并不增加数据,只是扩展了一些函数,那么如果此时需要提供自己的版本的移动构造函数以延续父类的移动语义,这时你可能就明确需要move函数了。如下代码

class PtrMemTest
{
public:
    int* p;
    PtrMemTest(int value)
    {
        std::cout<<"PtrMemTest"<<std::endl;
        p = new int(value);
    }
    ~PtrMemTest()
    {
        std::cout<<"~PtrMemTest"<<std::endl;
        if (p)
        {
            delete p;
            p = nullptr;
        }
    }
    PtrMemTest(const PtrMemTest& obj)
    {
        std::cout<<"PtrMemTest copy"<<std::endl;
        p = new int(*obj.p);
    }

    PtrMemTest(PtrMemTest&& obj)
    {
        std::cout<<"PtrMemTest move"<<std::endl;
        p = obj.p;
        obj.p = nullptr;
    }
};

class PtrMemTestDerive : public PtrMemTest
{
public:
    PtrMemTestDerive(int value) : PtrMemTest(value)
    {
        std::cout<<"PtrMemTestDerive"<<std::endl;
    }

    PtrMemTestDerive(const PtrMemTestDerive &obj) : PtrMemTest(obj)
    {
        std::cout<<"PtrMemTestDerive copy"<<std::endl;
    }

    PtrMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(obj)
    {
        std::cout<<"PtrMemTestDerive move"<<std::endl;
    }

    //........

};
void test()
{
    PtrMemTestDerive obj1(111);
    PtrMemTestDerive obj3(std::move(obj1));
}

    这段代码并没有按照你所期望的行使移动语义,因为PtrMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(obj)虽然子类中传入了右值引用,但是将obj像父类的移动构造(其实是拷贝构造)传递的时候,obj是个左值,所以并没有如预期的调用父类的移动构造函数,而是调用了父类的拷贝构造函数。所以这里move可以显示它的作用了。

class PtrMemTestDerive : public PtrMemTest
{
public:
    PtrMemTestDerive(int value) : PtrMemTest(value)
    {
        std::cout<<"PtrMemTestDerive"<<std::endl;
    }

    PtrMemTestDerive(const PtrMemTestDerive &obj) : PtrMemTest(obj)
    {
        std::cout<<"PtrMemTestDerive copy"<<std::endl;
    }

    PtrMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(std::move(obj))
    {
        std::cout<<"PtrMemTestDerive move"<<std::endl;
    }

    //........

};

新的子类用了move将obj继续作为右值传递给父类,顺利的调用父类的移动构造函数。

std::move

    上面的示例用到了很多STL库提供的新函数move函数,前面提到过的它的作用是强制将左值转换成右值引用。注意返回的是一个右值引用,而不是右值。但是直接使用move的返回值即得到一个右值,而不是一个右值引用。比较绕,看一下STL库中的注释:
Move as rvalue
Returns an rvalue reference to arg.
This is a helper function to force move semantics on values, even if they have a name: Directly using the returned value causes arg to be considered an rvalue.
大概翻译一下就是返回一个右值引用,直接用它的返回值就被当成一个右值用。
它的应用场景前面已经演示了,那么我们简单看一下它的实现。

template <class _Tp>
inline
typename remove_reference<_Tp>::type&&
move(_Tp&& __t)
{
    typedef typename remove_reference<_Tp>::type _Up;
    return static_cast<_Up&&>(__t);
}

    上面代码去掉了一些宏定义,留下了骨干内容,下面分析一下这个很短小的函数。首先它是一个模板函数,行参_Tp&& __t这里并不代表是_Tp类型的右值引用,而是有其特定规则的,这里后续在完美转发语义中会详细讲到,我们目前只需认为这里经过推倒后_Tp的类型可能是左值,左值引用,常量左值引用,和右值引用就好了。看下面的函数体通过字面意思remove_reference这个模板类的唯一作用是萃取出_Tp的原始非引用类型。也就是说_Up是个左值类型,这样最后返回的很明确,不管传入什么,最后传出都是一个右值引用。
    关于move我们在后续的完美转发中还会继续分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容