本文总结 copy elision、rvo、nvro 的概念及关系。


1. copy elision 与 rvo

copy elision,即 “复制省略”,是编译器的优化技术,包含两个场景:

  • 纯右值参数复制构造时的 copy elision。
  • 函数返回值优化( rvo,即 return value optimization )。

从 c++17 开始,强制要求编译器实现 copy elision。在 c++17 之前(c++11 / c++14),copy elision 依赖于编译器的具体实现,gcc 是默认支持 copy elision 的。


1.1 纯右值参数复制构造时的 copy elision

以下使用 gcc 13.2.0 。

#include <iostream>

struct A {
    int x;
    A(int _x) { x = _x; std::cout << "Call A(int)" << std::endl; }
    A(const A& a) { x = a.x; std::cout << "Call A(const A&)" << std::endl; }
};

int main() {
    [[maybe_unused]] A a = A(1); // 纯右值复制构造
    return 0;
}

1、对于 c++11 或 c++14

如果关闭编译器的 copy elision 优化,即加上 -fno-elide-constructors 选项,则输出是:

Call A(int)
Call A(const A&)

如果不关闭编译器的 copy elision 优化,则输出是:

Call A(int)


2、对于 c++17

无论是否关闭编译器的 copy elision 优化,输出都是:

Call A(int)

1.2 函数返回值优化(rvo)

以下使用 gcc 13.2.0 。

#include <iostream>

struct A {
    int x;
    A(int _x) { x = _x; std::cout << "Call A(int)" << std::endl; }
    A(const A& a) { x = a.x; std::cout << "Call A(const A&)" << std::endl; }
    A(A&& a) { x = a.x; std::cout << "Call A(A&&)" << std::endl; }
};

A getA() {
    return A(10);
}

int main() {
    [[maybe_unused]] A a = getA();
    return 0;
}

1、对于 c++11 或 c++14

如果关闭编译器的 copy elision 优化,即加上 -fno-elide-constructors 选项,则输出是:

Call A(int)
Call A(A&&)
Call A(A&&)

如果不关闭编译器的 copy elision 优化,则输出是:

Call A(int)

如果没有定义移动构造函数,则调用拷贝构造函数,可以编译器的 copy elision 可以优先掉移动构造或者拷贝构造。


2、对于 c++17

无论是否关闭编译器的 copy elision 优化,输出都是:

Call A(int)

2. 关于 nrvo

rvo 中有一种特殊的场景,叫 nrvo,即 name return value optimization,返回函数中已经命名的局部变量。c++17 标准对于 nrvo 没有强制规定,具体优化要看编译器的实现。

比如这样:

SomeType return_some_type() {
    SomeType x;   // x 就是一个 name return value
    return x;
}

这种情况下,编译器可以这样优化,在调用者的栈上构造出 x 这个局部变量,作为参数传为 return_some_type 使用,避免需要实际的 return x。


nrvo 比较复杂,需要分情况讨论。参考自此文章:《理解C++编译器中的 Copy elision 和 RVO 优化》 [1]:

1、返回局部变量,如果存在运行时依赖,则不一定会优化,具体要看编译器的实现。

比如这样:

SomeType get(bool flag) {
    if (flag) {
        SomeType x;
        // do some change
        return x;
    } else {
        SomeType y;
        return y;
    }
}

2、返回函数参数,不会优化。

3、返回全局变量,不会优化。

4、返回值使用 std::move 转换,不会优化。

比如这样:

SomeType get() {
    SomeType x;
    return std::move(x);
}

3. 问题探讨


3.1 有了 copy elision,移动构造还有意义吗?

还有意义,因为 copy elision 并不总是有效,特别 nrvo 的场景。

比如以下例子,编译器也做不出优化(编译器版本: g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0,默认采用的 c++ 版本是 #define __cplusplus 201703L):

#include <iostream>

struct A {
    int x;
    A(int _x) : x(_x) { std::cout << "A() " << _x << std::endl; }
    A(const A& other) : x(other.x) { std::cout << "A(A&) " << other.x << std::endl; }
    A(A&& other) : x(other.x) { std::cout << "A(A&&) " << other.x << std::endl; }
    ~A() { std::cout << "~A() " << x << std::endl; }
};

A f(int x) {
    A a(x);
    if (x == 0)
        return A(0);
    else if (x == 1)
        return A(1);
    else {
        return a;
    }
}

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

输出:

A() 300
A(A&&) 300
~A() 300
~A() 300

4. 拓展阅读

《Copy/move elision: C++ 17 vs C++ 11》

《理解C++编译器中的 Copy elision 和 RVO 优化》


5. 参考

[1] jiannanya​. 理解C++编译器中的 Copy elision 和 RVO 优化. Available at https://zhuanlan.zhihu.com/p/703789055, 2024-6-17.