返回值优化(RVONRVO)详解
一、什么是返回值优化(RVO/NRVO/URVO)?
返回值优化(Return Value Optimization, RVO)
命名返回值优化(Named Return Value Optimization, NRVO)
未命名返回值优化(Unnamed Return Value Optimization, NRVO)
Since C++17, URVO is mandatory and no longer considered a form of copy elision.
https://en.cppreference.com/w/cpp/language/copy_elision.html
是编译器在特定情况下对函数返回值的优化技术,旨在减少不必要的对象拷贝或移动操作,从而提高程序性能。那究竟什么是返回值优化呢?其根本就是拷贝省略。
- RVO:当函数返回一个局部对象时,编译器直接在调用者的栈帧中构造该对象,避免在函数返回时进行拷贝或移动。
- NRVO:当函数返回一个命名的局部对象时,编译器同样直接在调用者的栈帧中构造该对象,避免拷贝或移动。
- URVO:当函数返回一个未命名的临时对象时,编译器同样直接在调用者的栈帧中构造该对象,避免拷贝或移动
二、为什么需要 RVO?
在 C++ 中,函数返回对象时,通常会经历以下步骤:
- 在函数内部构造一个临时对象。
- 将临时对象拷贝或移动到调用者的栈帧中。
- 临时对象被销毁。
这种额外的拷贝或移动操作会增加程序的开销,尤其是在对象较大或构造、析构成本较高时。RVO通过消除这些不必要的操作来优化性能。总结就是RVO比移动语义更高效(直接构造,无额外操作)。
三、RVO代码分析
编译器通过以下方式实现NRVO/URVO:
- 直接构造:编译器在调用者的栈帧中直接构造返回对象,而不是在函数内部构造临时对象。
- 跳过拷贝或移动:由于对象已经在调用者的栈帧中构造,因此不需要进行拷贝或移动操作。
CMakeLists.txt
cmake_minimum_required(VERSION 3.31)
project(untitled)
#set(CMAKE_CXX_STANDARD 11)
#set(CMAKE_CXX_STANDARD 14)
#set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
## 打印当前使用的编译器信息
#message(STATUS "C compiler ID: ${CMAKE_C_COMPILER_ID}")
#message(STATUS "C++ compiler ID: ${CMAKE_CXX_COMPILER_ID}")
#
#if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# add_compile_options(-fno-elide-constructors)
# message(STATUS "GNU")
#elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
# add_compile_options(/Od)
# message(STATUS "MSVC")
#else ()
# message(WARNING "Unknown Compiler!")
#endif ()
# MSVC 和 MinGW (GCC) 禁用RVO优化
if (MSVC)
add_compile_options(/Od)
elseif (MINGW)
# MinGW (GCC): 禁用RVO优化
add_compile_options(-fno-elide-constructors)
else ()
message(WARNING "Unknown Compiler!")
endif ()
add_executable(untitled main.cpp)
main.cpp
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Default Constructor" << std::endl;
}
MyClass(const MyClass &) {
std::cout << "Copy Constructor" << std::endl;
}
MyClass(MyClass &&) noexcept {
std::cout << "Move Constructor" << std::endl;
}
~MyClass() {
std::cout << "Destructor" << std::endl;
}
};
// 返回一个命名的局部对象(可能触发NRVO)
MyClass createWithNRVO() {
MyClass obj; // 命名局部对象
return obj; // 直接在调用者的栈帧中构造
}
// 返回一个未命名的临时对象(可能触发URVO)
MyClass createWithURVO() {
return {}; // 直接构造临时对象并返回
// return MyClass();
}
int main() {
std::cout << "NRVO:" << std::endl;
MyClass b = createWithNRVO();
std::cout << "URVO:" << std::endl;
MyClass a = createWithURVO();
return 0;
}
当RVO触发时(通常在优化模式下):
NRVO:
Default Constructor
URVO:
Default Constructor
Destructor
Destructor
当RVO未触发时(使用-fno-elide-constructors
编译):
NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor
编译选项
要观察RVO的效果,也可以使用以下编译命令:
# 启用RVO(默认情况下大多数编译器会启用)
g++ -std=c++11 -O2 main.cpp -o untitled
# 禁用RVO以观察拷贝、移动操作
g++ -std=c++11 -O0 -fno-elide-constructors main.cpp -o untitled
四、NRVO/URVO 的触发条件
编译器是否应用 NRVO/URVO 取决于以下因素:
- 编译器实现:不同编译器(如 GCC、Clang、MSVC)对 NRVO/URVO 的支持程度不同。
- 优化级别:通常需要在
-O1
或更高优化级别下启用。 - 代码结构:
- 返回的必须是局部对象(匿名或命名)。
- 不能有多个返回路径(如
if-else
返回不同对象)。 - 不能有异常处理干扰(如
try-catch
)。
五、人为阻止 RVO
使用 std::move
谨慎,在返回局部对象时,显式使用 std::move(obj)
会阻止 NRVO(但可能触发移动语义)。不同编译器阻止RVO的范围也不同(MSVC有点特殊)。
- GCC编译器仅std::move()的对象会阻止NRVO
- MSVC编译器C++17及之前版本会影响全局阻止RVO,C++20恢复只影响局部对象NRVO
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Default Constructor" << std::endl;
}
MyClass(const MyClass &) {
std::cout << "Copy Constructor" << std::endl;
}
MyClass(MyClass &&) noexcept {
std::cout << "Move Constructor" << std::endl;
}
~MyClass() {
std::cout << "Destructor" << std::endl;
}
};
// 演示 NRVO(无 std::move)
MyClass createObjectNRVO() {
MyClass obj; // 局部对象
return obj; // 可能触发 NRVO
}
// 显式使用 std::move(阻止 NRVO,触发移动语义)
MyClass createObjectWithMove() {
MyClass obj;
return std::move(obj); // 强制移动语义,阻止 NRVO
}
int main() {
MyClass a = createObjectNRVO(); // 可能触发 NRVO(无拷贝/移动)
MyClass b = createObjectWithMove(); // 触发移动构造
return 0;
}
六、禁用RVO
微软官网MSVC编译器优化选项
- MSVC 没有提供像
-fno-elide-constructors
这样的选项来禁用 RVO。 - MSVC 会在 C++20 及以后版本强制执行 RVO,无法通过编译选项关闭。
可行的替代方案:
如果你目的是测试或研究对象的拷贝/移动行为,可以:
add_compile_options(/Od)
禁用规则总结:
- NRVO
- GCC、MSVC 可以选择禁用,不受版本影响
- URVO
- MSVC C++11 强制开启,无法禁用
- GCC C++17 强制开启,无法禁用
- MSVC 会在 C++20 及以后版本强制执行 RVO (NRVO/URVO),无法通过编译选项关闭。
GCC
C++11
NRVO:
Default Constructor
Move Constructor
Destructor
Move Constructor
Destructor
URVO:
Default Constructor
Move Constructor
Destructor
Destructor
Destructor
C++14
NRVO:
Default Constructor
Move Constructor
Destructor
Move Constructor
Destructor
URVO:
Default Constructor
Move Constructor
Destructor
Destructor
Destructor
C++17
NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor
C++20
NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor
MSVC
C++11
NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor
C++14
NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor
C++17
NRVO:
Default Constructor
Move Constructor
Destructor
URVO:
Default Constructor
Destructor
Destructor
C++20
NRVO:
Default Constructor
URVO:
Default Constructor
Destructor
Destructor
Sync CMake Changes
Ctrl+Shift+O
七、如何验证 RVO 是否发生?
- 通过观察构造函数和拷贝、移动构造函数的调用情况。
- 使用编译器选项(如 GCC 的
-fdump-tree-optimized
)查看优化后的中间代码。
C:\Program Files\JetBrains\CLion 2025.1\bin\mingw\bin
g++ -std=c++11 -fdump-tree-optimized -O0 main.cpp -o untitled
g++ -std=c++11 -fdump-tree-optimized -O2 main.cpp -o untitled
八、练习题
#include <iostream>
struct Noisy {
Noisy() { std::cout << "constructed at " << this << std::endl; }
Noisy(const Noisy &) { std::cout << "copy-constructed" << std::endl; }
Noisy(Noisy &&) noexcept { std::cout << "move-constructed" << std::endl; }
~Noisy() { std::cout << "destructed at " << this << std::endl; }
};
Noisy f() {
Noisy v = Noisy(); // (until C++17) copy elision initializing v from a temporary;
// the move constructor may be called
// (since C++17) "guaranteed copy elision"
return v; // copy elision ("NRVO") from v to the result object;
// the move constructor may be called
}
void g(Noisy arg) {
std::cout << "&arg = " << &arg << std::endl;
}
int main() {
Noisy v = f(); // (until C++17) copy elision initializing v from the result of f()
// (since C++17) "guaranteed copy elision"
std::cout << "&v = " << &v << std::endl;
g(f()); // (until C++17) copy elision initializing arg from the result of f()
// (since C++17) "guaranteed copy elision"
}