我理解的右值引用
右值,顾名思义,可以先理解为 “=” 号右边的值,通常是个常量或者临时变量,比如代码:
int foo = 123; //foo是lvalue, 123是rvalue
int plus(int left, int right) {
return left + right;
}
int result = plus(1, 2); //result是lvalue, plus函数的计算结果是一个rvalue
右值引用提供了什么能力
右值引用提供了对右值进行操作的能力,以此来达到对临时变量进行复用,避免无意义的临时变量深拷贝。
举一个例子,在没有右值引用的年代,我们用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做运算,类似于下面的代码:
auto str = std::string("aaa");
str += "bbb";
str += "ccc";
str += "ddd";
右值引用配合unique_ptr和pImpl写法
核心思想是,把占用内存空间大的对象封装到一个impl嵌套类里面,用unique_ptr进行维护,unique_ptr有独占语义,并且如果不自定义销毁函数的话,他所占用的空间和一个裸指针无异,享受方便封装的同时不带来任何的额外负担。
所有对象都封装到impl里面的另一个好处是,move语义时可以很方便的实现,只需要move pImpl本身即可。代码如下:
#include <memory>
#include <iostream>
int g_getObjectId() {
static int seed = 0;
return seed++;
}
int g_getObjectId2() {
static int seed = 0;
return seed++;
}
class Widget {
struct Impl {
int m_objId;
Impl() {
m_objId = g_getObjectId();
std::cout << "Widget::Impl " << m_objId << " constructed" << std::endl;
}
~Impl() {
std::cout << "Widget::Impl " << m_objId << " destructed" << std::endl;
}
};
public:
Widget(Widget&& w) : m_pImpl(std::move(w.m_pImpl)), m_selfObjectId(g_getObjectId2()) { //右值构造函数
std::cout << "Widget " << m_selfObjectId << " constructed" << std::endl;
}
Widget() : m_pImpl(new Impl()), m_selfObjectId(g_getObjectId2()) {
std::cout << "Widget " << m_selfObjectId << " constructed" << std::endl;
}
~Widget() {
std::cout << "Widget " << m_selfObjectId << " destructed" << std::endl;
}
int getObjectId() const {
return m_pImpl->m_objId;
}
private:
std::unique_ptr<Impl> m_pImpl;
int m_selfObjectId;
};
void foo(Widget w) {
std::cout << "foo widget for object " << w.getObjectId() << std::endl;
}
void foo2(const Widget& w) {
std::cout << "foo widget for const reference" << w.getObjectId() << std::endl;
}
Widget makeWidget() { //模拟工厂
Widget w;
return std::move(w); //w以右值返回,会构造一个新的Widget,但是因为右值有右值构造函数,成本很低
}
Widget makeWidgetWithoutMove() { //模拟工厂
Widget w;
return w; //利用编译器的RVO(返回值优化),可能不会有新的Widget被构造
}
Widget makeWidgetWithoutMove2(int type) {
Widget a, b;
if (type == 0) {
return a;
}
else {
return b;
}
}
int main() {
{
Widget w(makeWidget());
foo(std::move(w)); //move语义,表示w下面不再使用
}
std::cout << "--------------------------------------------------" << std::endl;
{
Widget w(makeWidgetWithoutMove());
foo(std::move(w)); //move语义,表示w下面不再使用
}
std::cout << "--------------------------------------------------" << std::endl;
{
Widget w(makeWidgetWithoutMove2(0));
foo(std::move(w)); //move语义,表示w下面不再使用
}
std::cout << "--------------------------------------------------" << std::endl;
{
Widget w2(makeWidget());
foo2(std::move(w2));//move语义,表示w2下面不再使用
}
std::cout << "--------------------------------------------------" << std::endl;
{
Widget w3(makeWidget());
foo2(w3); //直接传递w
}
}
依赖unique_ptr的独占语义,只需要在Widget的右值构造函数中把右值的m_pImpl作为参数给新的Widget的m_pImpl构造即可,unique_ptr会把右值的指针设置为空,以此来保证独占性。右值销毁时,什么都不会做,其指向的m_pImpl内存顺利让渡到新的Widget
下面是程序的输出:
Widget::Impl 0 constructed
Widget 0 constructed
Widget 1 constructed
Widget 0 destructed
Widget 2 constructed
foo widget for object 0
Widget 2 destructed
Widget::Impl 0 destructed
Widget 1 destructed
--------------------------------------------------
Widget::Impl 1 constructed
Widget 3 constructed
Widget 4 constructed
foo widget for object 1
Widget 4 destructed
Widget::Impl 1 destructed
Widget 3 destructed
--------------------------------------------------
Widget::Impl 2 constructed
Widget 5 constructed
Widget::Impl 3 constructed
Widget 6 constructed
Widget 7 constructed
Widget 6 destructed
Widget::Impl 3 destructed
Widget 5 destructed
Widget 8 constructed
foo widget for object 2
Widget 8 destructed
Widget::Impl 2 destructed
Widget 7 destructed
--------------------------------------------------
Widget::Impl 4 constructed
Widget 9 constructed
Widget 10 constructed
Widget 9 destructed
foo widget for const reference4
Widget 10 destructed
Widget::Impl 4 destructed
--------------------------------------------------
Widget::Impl 5 constructed
Widget 11 constructed
Widget 12 constructed
Widget 11 destructed
foo widget for const reference5
Widget 12 destructed
Widget::Impl 5 destructed
可以看到4次测试,真正的内存块Impl都是只构造了一次。
move到底做了啥
根据 《modern effective c++》 中的描述,move啥也没做,只是把参数做了一个强制右值转换,返回就完了。那么 unique_ptr为何move之后,老的值就变成null了呢。
写出上面的测试代码后,我理解了这个问题,其实靠的不是move,而是类的右值构造函数,大概可以模拟一下unique_ptr的代码:
template<typename T>
class unique_ptr {
public:
unique_ptr(unique_ptr<T>&& rv) {
m_pRaw = rv.m_pRaw;
rv.m_pRaw = nullptr;
}
private:
T* m_pRaw;
}
根据上面的代码,当写出auto newptr = std::move(oldptr)时,oldptr作为一个右值引用被传入newptr的构造函数,然后其原始指针的地址被修改为null了。
RVO(返回值优化)和右值引用
第二次测试中,我调用了makeWidgetWithoutMove作为工厂函数,从输出来看,他是构造Widget次数最少的一种方法,这种性能的提升得益于编译器的RVO(返回值优化)策略,但是这个策略比较复杂,比如如下代码:
Widget makeWidgetWithoutMove() { //模拟工厂
Widget w;
return w; //利用编译器的RVO(返回值优化),可能不会有新的Widget被构造
}
Widget w(makeWidgetWithoutMove());
foo(std::move(w)); //move语义,表示w下面不再使用
在编译器经过优化后,会生成如下代码:
void makeWidgetWithoutMove(Widget &_result) { //模拟工厂
}
Widget w;
makeWidgetWithoutMove(w);
foo(std::move(w)); //move语义,表示w下面不再使用
这种在函数开头就定义一个Widget实例,在函数结尾返回的例子,通常可以被RVO,但是稍微复杂一点,比如需要根据不同的条件return不同的local var时,RVO就不一定奏效了,如下面的代码所示
Widget makeWidgetWithoutMove2(int type) {
Widget a, b;
if (type == 0) {
return a;
}
else {
return b;
}
}
Widget w(makeWidgetWithoutMove2(0));
foo(std::move(w));
本质上来讲,makeWidgetWithoutMove2 函数内,通过一个变量判断返回哪一个local var,那么编译器就无法确定要用&_result 代替哪一个变量,优化就无从谈起。
所以,除了RVO以外,在一些复杂的场景,我们还是需要右值引用来为我们解决函数返回值的性能问题。
总结
纸上得来终觉浅,绝知此事要躬行。右值引用看过好多次了,每次都囫囵吞枣,希望这次代码练习和写作可以把它牢牢掌握。
参考
- C++的返回值优化以及右值拷贝 sq.163yun.com/blog/articl…
- 一次性搞定右值,右值引用(&&),和move语义 juejin.cn/post/684490…
- 《modern effective c++》
- 《深度探索C++对象模型》