返回值优化 | C & C++

本文介绍了 C++ 中的返回值优化。

RVO(return value optimization,返回值优化)

首先看一下这一段代码:

1
2
3
4
5
6
7
Obj fun() {
return Obj();
}

int main() {
Obj obj = fun();
}

在编译器不进行优化的情况下,这段代码一共会调用:

  • 1 次构造函数:对应代码 Obj()
  • 2 次拷贝构造函数:
    • 函数返回使用到的临时对象的拷贝构造
    • 对象 obj 的拷贝构造
  • 3 次析构函数:将上述构造出的对象析构

当一个未具名且未绑定到任何引用的临时变量被移动或复制到一个相同的对象时,拷贝和移动构造可以被省略。当这个临时对象在被构造的时候,它会直接被构造在将要拷贝/移动到的对象。编译器明确知道函数会返回哪一个局部对象,那么编译器会把存储这个局部对象的地址和存储返回临时对象的地址进行复用,也就是说避免了从局部对象到临时对象的拷贝操作。

在这个例子中,代码会被优化成这样:

1
2
3
4
5
6
7
8
void fun(Obj &_obj) {
_obj.Obj::Obj(); // 构造函数
}

int main() {
Obj obj; // 仅定义不构造
fun(obj);
}

另外,这种针对未具名临时对象的拷贝构造优化同样也发生在容器操作中,在使用 vector::push_back 操作时,根据 push_back 的内容,会执行不同的步骤:

  • 编译器优化:如果 push_back 的参数是 T 的一个对象,那么会直接调用拷贝构造函数在 vector 中构造对象,这时行为和 emplace_back 一致
  • 正常行为:如果 push_back 的参数和 T 的某一个 implicit 构造函数一致,那么就会先调用该构造函数构造一个临时对象,然后调用移动构造函数将该临时对象移动到 vector

NRVO(named return value optimization,具名返回值优化)

RVO 的优化仅在不具名对象返回的情况下比较有用,再看下这一段代码:

1
2
3
4
5
6
7
8
Obj fun() {
Obj obj;
return obj;
}

int main() {
Obj obj = fun();
}

在编译器不进行优化的情况下,这段代码一共会调用:

  • 1 次构造函数:fun 函数中对象 obj 的拷贝构造
  • 2 次拷贝构造函数:
    • 函数返回使用到的临时对象的拷贝构造
    • main 函数中对象 obj 的拷贝构造
  • 3 次析构函数:将上述构造出的对象析构

如果进行 RVO,代码会被优化成这样:

1
2
3
4
5
6
7
8
9
10
void fun(Obj &_obj) {
Obj obj(); // 构造函数
_obj.Obj::Obj(obj); // 拷贝构造函数
return;
}

int main() {
Obj obj; // 仅定义不构造
fun(obj);
}

仍然存在一次多余的拷贝构造,优化并不彻底。但如果进行 NRVO,代码会被优化成这样:

1
2
3
4
5
6
7
8
9
10
void fun(Obj &_obj) {
Obj obj(); // 构造函数
_obj.Obj::Obj(obj); // 拷贝构造函数
return;
}

int main() {
Obj obj; // 仅定义不构造
fun(obj);
}

优化失效

返回对象不为局部对象

当返回的对象不是局部对象而是全局变量、函数参数或者成员变量时,会禁用 (N)RVO。

返回对象类型和函数返回类型不同

这时会触发隐式类型转换,会禁用 (N)RVO。

运行时依赖

当编译器无法单纯通过函数来决定返回哪个实例对象时,会禁用 (N)RVO。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
Obj fun(bool flag) {
Obj o1;
Obj o2;
if (flag) {
return o1;
}
return o2;
}

int main() {
Obj obj = fun(true);
}

如果逻辑上允许,最好优化成这样:

1
2
3
4
5
6
7
8
9
10
11
12
Obj fun(bool flag) {
Obj obj;
if (flag) {
return obj;
}
obj.n = 10;
return obj;
}

int main() {
Obj obj = fun(true);
}

存在赋值行为

(N)RVO 只能在从返回值创建对象时发送,在现有对象上使用 operator= 而不是拷贝/移动构造函数,这样是不会进行优化的。

1
2
3
4
5
6
7
8
Obj fun() {
return Obj();
}

int main() {
Obj obj;
obj = fun();
}

使用std::move()返回

在返回值上调用 std::move() 进行返回是一种错误的方式。它会尝试强制调用移动构造函数,但这样会导致 (N)RVO 失效。因为即使没有显示调用 std::move(),编译器优化中也会执行 move 操作。

参考

编译器之返回值优化

作者

zhongtian

发布于

2022-03-28

更新于

2023-12-16

许可协议

评论