为何C++中foo2()返回对象时调用拷贝构造而非移动构造?
这是个很典型的移动语义触发场景问题,核心在于右值引用的判定规则,咱们一步步拆解:
1. 移动构造函数的触发前提
移动构造函数tNode(tNode&& node)的调用条件是:传入的实参必须是右值(包括纯右值、将亡值)。只有当编译器识别到表达式是"可以被安全移动的临时对象/即将销毁的对象"时,才会优先选择移动构造,而非拷贝构造。
2. foo()中触发移动构造的原因
看foo()的实现:
tNode foo() { tNode node; return node; }
这里返回的node是局部非静态对象,虽然它本身是左值,但C++标准有个特殊规则:当函数返回的是与局部对象类型完全匹配的对象时,编译器会自动将这个局部左值视为将亡值(一种特殊的右值)——因为这个局部对象马上就要被销毁了,移动它不会有任何问题。所以即使你用-fno-elide-constructors关闭了返回值优化(RVO),编译器依然会调用移动构造来创建返回的临时对象,这就是你看到foo()输出里两次移动构造的原因。
3. foo2()中调用拷贝构造的关键原因
再看foo2()的实现:
tNode foo2() { std::unique_ptr<tNode> up = std::make_unique<tNode>(20); return *up; }
这里的*up是std::unique_ptr的operator*返回的结果,而unique_ptr::operator*的返回类型是**tNode&(左值引用)**——因为unique_ptr管理的是堆上的一个持久化对象(直到unique_ptr销毁前,这个对象都存在),*up是对这个堆对象的左值引用,编译器不会把它当作"即将销毁的临时对象",自然不会将其视为右值。
既然*up是左值,编译器就只能调用接受左值引用的拷贝构造函数tNode(const tNode& node)来创建返回的临时对象,这就是你看到foo2()输出里拷贝构造被调用的原因。
4. 如何让foo2()触发移动构造?
如果想让foo2()也调用移动构造,可以显式地把*up转换成右值引用,用std::move():
tNode foo2() { std::unique_ptr<tNode> up = std::make_unique<tNode>(20); return std::move(*up); // 显式将左值转为右值引用 }
这样编译器就会识别到这是一个可移动的右值,从而调用移动构造函数。不过要注意:移动后原堆对象的状态会被置为有效但未定义的状态,但这里unique_ptr在函数结束后会销毁原对象,所以不会有问题。
对应你的输出验证
从你提供的编译输出也能完全对应流程:
- foo()流程:默认构造局部node → 移动构造返回临时对象 → 销毁局部node → 移动构造初始化n1 → 销毁临时对象 → 最后销毁n1
- foo2()流程:默认构造堆对象 → 拷贝构造返回临时对象 → 销毁堆对象 → 移动构造初始化n2 → 销毁临时对象 → 最后销毁n2
内容的提问来源于stack exchange,提问作者pchrys

