C++返回包含多个大对象的tuple的最优方式及测试疑问
问题1:为什么foo()会触发A的拷贝构造?
首先要明确RVO的作用边界:返回值优化(包括C17起的强制复制消除)仅负责消除返回值对象本身的拷贝/移动操作,不会干预返回值对象内部元素的构造逻辑。
你在foo()中写的return {a, b}属于用花括号初始化列表直接构造返回的std::tuple<A, B>对象,这一步因为RVO的存在,tuple确实会直接构造在main函数的接收地址上,不会发生tuple本身的拷贝/移动。但问题出在tuple构造时对元素的处理:
C只有在return语句的操作数直接是一个局部自动变量名时,才会隐式将其转为右值尝试移动。但你这里不是直接return a,而是把a作为构造tuple的参数,此时a仍然是左值,tuple的构造函数会匹配左值引用的拷贝构造来初始化内部的A元素,所以你会看到拷贝构造的输出。编译器没有义务自动把即将销毁的局部左值在作为构造参数时转为右值,这个行为是C++标准明确规定的。
问题2:为什么quux()会多一次移动操作?
你在quux()里写的return std::move(std::tuple<A, B>{ std::move(a), std::move(b) });属于典型的画蛇添足操作,直接破坏了强制复制消除的条件。
C++17规定的强制复制消除只对纯右值作为返回值的场景生效:当你直接返回std::tuple<A, B>{...}这个临时对象(纯右值)时,编译器可以直接把这个临时对象构造在返回值的内存地址上,不需要任何拷贝或移动。但你给这个临时对象套了一层std::move,会把纯右值转为将亡值(右值引用类型),此时不再满足强制复制消除的要求,编译器只能:
- 先构造内部的临时tuple对象,用
std::move(a)移动构造内部的A元素(第一次移动) - 再用这个临时tuple(右值)移动构造返回的tuple对象,内部的A元素再发生一次移动(第二次移动)
- 临时tuple销毁触发一次A的析构,返回的tuple最终在main中销毁再触发一次,所以对应两次析构的输出。
补充说明
bar()的写法是当前场景下的最优实现:return {std::move(a), std::move(b)}直接用右值初始化tuple的元素,仅触发一次A的移动构造,同时返回值是纯右值,满足强制复制消除的要求,没有tuple本身的额外开销,完全符合你避免大对象拷贝的需求。
内容的提问来源于stack exchange,提问作者jwezorek

