C++函数返回大型对象:按const引用还是按值接收更优?
关于值接收与const引用接收返回值的疑问
我一直认为,当接收函数按值返回的大型对象时,使用const引用是正确做法——既能避免临时对象到目标变量的额外拷贝,还能延长临时对象的生命周期,Herb Sutter的文章里也提到过这一点。但我通过汇编对比发现,在-O0优化级别下,按值接收临时返回值的代码反而更短。我了解RVO和NRVO优化,但测试是在-O0下做的,想知道这些优化在-O0下是否还会执行?另外有没有更适合用const引用接收返回值的场景?
测试用C++代码
#include <string> using namespace std; string foo() { return "test"; } void bar() { auto y = foo(); } void dar() { const auto& z = foo(); }
GCC 14.2生成的x86-64汇编代码
bar()函数汇编
bar(): push rbp mov rbp, rsp sub rsp, 32 lea rax, [rbp-32] mov rdi, rax call foo[abi:cxx11]() lea rax, [rbp-32] mov rdi, rax call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor] leave ret
dar()函数汇编
dar(): push rbp mov rbp, rsp sub rsp, 48 lea rax, [rbp-48] mov rdi, rax call foo[abi:cxx11]() lea rax, [rbp-48] mov QWORD PTR [rbp-8], rax lea rax, [rbp-48] mov rdi, rax call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor] leave ret
dar()比bar()多的两条指令:
lea rax, [rbp-48] mov QWORD PTR [rbp-8], rax
问题解答
1. RVO/NRVO在-O0下是否生效?
不会。-O0是GCC的默认无优化级别,编译器会严格遵循C++标准的抽象语义生成代码,不会执行RVO(返回值优化)或NRVO(具名返回值优化)这类改变对象创建位置的优化。这也是两种写法汇编存在差异的核心原因——无优化时编译器会忠实地处理每一步语义。
2. 为什么值接收的汇编更短?
在-O0下:
- 对于
auto y = foo();,C++标准允许拷贝省略:用临时对象初始化同类型对象时,可直接在目标对象的内存位置构造临时对象,无需额外拷贝。因此编译器直接在栈上为y分配空间,调用foo()构造对象,最后析构y,流程更简洁。 - 对于
const auto& z = foo();,编译器需要先创建foo()返回的临时对象,再让引用z指向它。栈上需要额外分配存储临时对象地址的引用变量空间,因此多了两条保存地址的指令,导致汇编代码更长。
3. 适合用const引用接收返回值的场景
虽然直接接收返回值时,值接收通常更优(开启优化后RVO会让两种写法的汇编几乎一致),但以下场景更适合用const引用:
- 复用临时对象生命周期:如果需要在多个作用域中使用返回的临时对象,
const引用可将其生命周期延长至引用的作用域结束,避免多次构造/析构。 - 函数参数传递:将返回对象传递给其他函数时,用
const引用传递可避免拷贝,例如:void process(const string& s); // 调用时直接传递临时对象的引用,无拷贝 process(foo()); - 无法触发拷贝省略的场景:当返回值需要转换为其他类型,或编译器无法执行拷贝省略的特殊情况,
const引用可避免创建额外的拷贝对象。
注意:非const引用无法绑定到临时对象,只有const引用或右值引用可以。
内容的提问来源于Stack Exchange,提问作者bobeff
相关产品推荐
相关产品推荐

