我理解的右值引用
右值,顾名思义,可以先理解为 “=” 号右边的值,通常是个常量或者临时变量,比如代码:
右值引用提供了什么能力
右值引用提供了对右值进行操作的能力,以此来达到对临时变量进行复用,避免无意义的临时变量深拷贝。
举一个例子,在没有右值引用的年代,我们用std::string时常常会想起大师的教导,不要写出下面的代码: “std::string(“aaa”) + std::string(“bbb”) + std::string(“ccc”) + std::string(“ddd”)”
这种写法是为人所不齿的,因为会产生无数个临时变量。
但是有了右值引用之后, “std::string(“aaa”) + std::string(“bbb”)” 是两个右值相加,那么可以把right 累加到 left上,返回left, left又是一个右值, 继续和后面的string做运算,类似于下面的代码:
右值引用配合unique_ptr和pImpl写法
核心思想是,把占用内存空间大的对象封装到一个impl嵌套类里面,用unique_ptr进行维护,unique_ptr有独占语义,并且如果不自定义销毁函数的话,他所占用的空间和一个裸指针无异,享受方便封装的同时不带来任何的额外负担。
所有对象都封装到impl里面的另一个好处是,move语义时可以很方便的实现,只需要move pImpl本身即可。代码如下:
|
|
依赖unique_ptr的独占语义,只需要在Widget的右值构造函数中把右值的m_pImpl作为参数给新的Widget的m_pImpl构造即可,unique_ptr会把右值的指针设置为空,以此来保证独占性。右值销毁时,什么都不会做,其指向的m_pImpl内存顺利让渡到新的Widget
下面是程序的输出:
可以看到4次测试,真正的内存块Impl都是只构造了一次。
move到底做了啥
根据 《modern effective c++》 中的描述,move啥也没做,只是把参数做了一个强制右值转换,返回就完了。那么 unique_ptr为何move之后,老的值就变成null了呢。
写出上面的测试代码后,我理解了这个问题,其实靠的不是move,而是类的右值构造函数,大概可以模拟一下unique_ptr的代码:
根据上面的代码,当写出auto newptr = std::move(oldptr)时,oldptr作为一个右值引用被传入newptr的构造函数,然后其原始指针的地址被修改为null了。
RVO(返回值优化)和右值引用
第二次测试中,我调用了makeWidgetWithoutMove作为工厂函数,从输出来看,他是构造Widget次数最少的一种方法,这种性能的提升得益于编译器的RVO(返回值优化)策略,但是这个策略比较复杂,比如如下代码:
在编译器经过优化后,会生成如下代码:
这种在函数开头就定义一个Widget实例,在函数结尾返回的例子,通常可以被RVO,但是稍微复杂一点,比如需要根据不同的条件return不同的local var时,RVO就不一定奏效了,如下面的代码所示
本质上来讲,makeWidgetWithoutMove2 函数内,通过一个变量判断返回哪一个local var,那么编译器就无法确定要用&_result 代替哪一个变量,优化就无从谈起。
所以,除了RVO以外,在一些复杂的场景,我们还是需要右值引用来为我们解决函数返回值的性能问题。
总结
纸上得来终觉浅,绝知此事要躬行。右值引用看过好多次了,每次都囫囵吞枣,希望这次代码练习和写作可以把它牢牢掌握。
参考
- C++的返回值优化以及右值拷贝 https://sq.163yun.com/blog/article/170332301127245824
- 一次性搞定右值,右值引用(&&),和move语义 https://juejin.im/post/59c3932d6fb9a00a4b0c4f5b
- 《modern effective c++》
- 《深度探索C++对象模型》