C++函数返回左值对象地址相同未触发移动构造原因
观测到的跨栈帧对象地址相同、返回过程无移动构造调用两个现象,本质是命名返回值优化(NRVO, Named Return Value Optimization) 生效,属于C++标准明确允许的拷贝消除(Copy Elision)优化场景。
核心机制说明
跨栈帧对象地址完全一致的原因:
按照未优化的执行逻辑,get_vec()的局部变量vec本应分配在get_vec()的独立栈帧中,函数返回时需要通过拷贝/移动把vec的内容转移到main()栈帧的v对象内存里。但NRVO生效时,编译器会直接跳过这个临时对象的分配步骤:提前把返回值的存储位置分配在调用方(即main()中v所在的栈地址),get_vec()函数内部对vec的所有操作,本质都是直接在v的内存地址上执行的,从头到尾只存在一个vector对象,自然打印出的栈地址、内部堆内存指针data()完全相同。未调用移动构造函数的原因:
拷贝消除的优化优先级高于移动语义。当NRVO触发条件满足时,编译器会直接省略掉拷贝/移动构造的调用步骤,对象从一开始就构造在最终的目标内存位置,不存在“把一个对象的内容转移到另一个对象”的过程,自然不会触发移动构造。这不是移动操作执行太快没被观测到,是根本没有发生移动/拷贝构造的流程。
对应代码的NRVO触发条件
当前测试代码完全满足NRVO的触发要求:
- 函数返回类型和局部返回对象的类型完全一致,都是
std::vector<int> - 函数所有返回路径返回的都是同一个局部命名对象(代码只有一个返回分支,返回的就是局部变量
vec) - 返回的局部对象不是函数入参,也没有被
volatile修饰
补充说明:C++17标准只强制要求了匿名临时对象的拷贝消除,NRVO属于编译器可选的优化项,但目前GCC、Clang、MSVC等所有主流编译器,哪怕在O0无优化级别下都会默认开启NRVO。如果想要观测未优化的返回流程,可以给编译器加参数禁用拷贝消除:比如GCC/Clang使用
-fno-elide-constructors参数重新编译,就能看到两个对象地址不同,返回时会触发移动构造,原局部对象vec的内部data()指针会被置空,v会接管原vec的堆内存。
测试代码
#include <iostream> #include <vector> std::vector<int> get_vec() { std::vector<int> vec = { 1, 2, 3, 4, 5 }; std::cout << "get_vec():\n" << &vec << '\n' << vec.data() << "\n\n"; return vec; } int main() { std::vector<int> v = get_vec(); std::cout << "main():\n" << &v << '\n' << v.data() << "\n\n"; return 0; }
观测到的运行输出
OUTPUT: get_vec(): 0x7a07ee93baa0 0x226b290 main(): 0x7a07ee93baa0 0x226b290
内容的提问来源于stack exchange,提问作者filipcimo

