C++ 返回值优化 RVO

最近在调试代码时,发现拷贝和移动系列构造函数的调用和预期不太一样,经过查阅相关资料,发现是返回值优化(RVO)从中做梗。了解了事实真相后,今天就和大家聊聊这个 RVO。

引子

直接看一个例子:

#include <iostream>

class A {
public:
    A (){
        std::cout << "A(): addr= " << this << std::endl;
    }
    A (const A&) {
        std::cout << "A(const A&): addr= " << this << std::endl;
    }
    A& operator=(const A&) {
        std::cout << "operator=(&&): addr= " << this << std::endl;
        return *this;
    }
    // A (A&&) {
    //     std::cout << "A(A&&): addr= " << this << std::endl;
    // }
    // A& operator=(A&&) {
    //     std::cout << "operator=(A&&): addr=" << this << std::endl;
    //     return *this;
    // }
    ~A() {
        std::cout << "~A(): addr=" << this << std::endl;
    };
};

// A makeA () {
//     A a;
//     return a;
// } 
A makeA () {
    return A();
} 

int main () {
    A a = makeA();
    return 0;
}

按照我之前的理解,这里应该发生一次默认构造函数、两次拷贝构造函数和三次析构函数的调用:

  • makeA () 中,A() 调用构造函数,产生对象1;
  • makeA () 中,return A() 产生一次拷贝,调用拷贝构造函数,产生临时对象2;
  • 离开 makeA(),对象1调用析构函数进行释放;
  • main () 函数中,A a = makeA() 产生一次拷贝,调用拷贝构造函数,将临时对象2拷贝给对象 a;
  • 离开main () 函数,临时对象2和对象a释放,各调用一次析构函数。

但是,编译运行窗口打印如下,只调用了一次默认构造函数和一次析构函数:

A(): addr= 0x7fffdf058eb7
~A(): addr=0x7fffdf058eb7

这是因为编译器默认开启了返回值优化(RVO),可以通过编译选项 -fno-elide-constructors 关闭 RVO(这里使用的编译器是 g++),关闭 RVO 重新编译运行,输出结果符合预期:

A(): addr= 0x7ffc7e2cec07
A(const A&): addr= 0x7ffc7e2cec37
~A(): addr=0x7ffc7e2cec07
A(const A&): addr= 0x7ffc7e2cec36
~A(): addr=0x7ffc7e2cec37
~A(): addr=0x7ffc7e2cec36

可以看到,RVO 减少了两次拷贝和两次析构。同样的,对于移动构造也是一样的,打开 A 中自定义的移动构造函,a = makeA() 这里会调用移动构造函数初始化 a(自定义了拷贝操作会阻止编译器生成移动操作,可以查阅 Understand special member function generation 了解更多。

自定义 move 操作,关闭 RVO:

A(): addr= 0x7ffd575627e7
A(A&&): addr= 0x7ffd57562817
~A(): addr=0x7ffd575627e7
A(A&&): addr= 0x7ffd57562816
~A(): addr=0x7ffd57562817
~A(): addr=0x7ffd57562816

自定义 move 操作,开启 RVO:

A(): addr= 0x7ffc4f79c227
~A(): addr=0x7ffc4f79c227

和拷贝一样,RVO 也同样消除了两次移动构造函数的调用。因此返回值优化包括返回值的拷贝优化和移动优化。

返回值优化 RVO

返回值优化(RVO:Return Value Optimization)是编译器的一种编译优化,通过该优化可以减少函数返回时产生临时对象,从而消除部分拷贝或移动操作,进而提高代码性能。

A makeA () {
    return A();
} 

int main () {
    A a = makeA();
    return 0;
}

为了减少不必要的拷贝或者移动,RVO 试图减少临时对象的分配,将最终的对象直接以引用的方式传递到函数中,直接对最终的对象进行构造。RVO 的效果相当于将 func 函数优化如下:

void makeA (A& a) {
    a.A::A();
} 

调用点直接传入要构造的对象,也就是相当于直接构造出最终的对象,消除了两次拷贝或者移动操作,也就自然消除了两次析构操作。

RVO 有一个变种 NRVO(Named Return Value Optimization),与 RVO 的区别其实就是函数中返回的对象已经具名了,类似这样:

A makeA () {
    A a
    return a;
} 

NRVO 的原来和 RVO 基本一致。

Effective Modern C++ 中将 RVO 优化的条件概括为两点:

  • 局部对象的类型和返回类型相同
  • 局部对象就是返回值

我们开头的例子就完全满足这两个条件。

RVO 限制

下面看几种 RVO 限制的场景。

返回 std::move(a)。 会使 RVO 失效,会多产生一次拷贝和一次析构。

A makeA () {
    A a
    return std::move(a);
} 

// output
A(): addr= 0x7ffc19323917
A(A&&): addr= 0x7ffc19323947
~A(): addr=0x7ffc19323917
~A(): addr=0x7ffc19323947

调用点不是对象初始化,而是赋值。

// ....
A makeA () {
    A a;
    return a;
} 

int main () {
    A a;
    a = makeA();
    return 0;
}

// output
A(): addr= 0x7ffcfd28bab6
A(): addr= 0x7ffcfd28bab7
operator=(A&&): addr=0x7ffcfd28bab6
~A(): addr=0x7ffcfd28bab7
~A(): addr=0x7ffcfd28bab6

返回的局部对象存在分支判断。

// ...

A makeA (bool flag) {
    A a1, a2;
    if (flag) {
        return a1;
    } else {
        return a2;
    }
} 

int main () {
    int x;
    A a = makeA(x);
    return 0;
}
// output
A(): addr= 0x7ffd5be33b56
A(): addr= 0x7ffd5be33b57
A(A&&): addr= 0x7ffd5be33b83
~A(): addr=0x7ffd5be33b57
~A(): addr=0x7ffd5be33b56
~A(): addr=0x7ffd5be33b83

总之, 编译器的返回值优化也是有限制的,有些场景不一定生效。这里 列举了更多 RVO 限制的场景,有兴趣的可以前去查看。

参考

  • https://docs.microsoft.com/en-us/previous-versions/ms364057(v=vs.80)?redirectedfrom=MSDN
  • https://moodle.ufsc.br/pluginfile.php/2377667/mod_resource/content/0/Effective_Modern_C__.pdf
  • https://mp.weixin.qq.com/s/LwnDtK6HNZo_StIxQ5yJhA
  • 18
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值