问题描述
我正在创建一个使用通用转发的库组件。 这在大多数情况下都可以正常工作,但我遇到过我们的 linux 构建(似乎)错误地使用复制构造函数而不是移动构造函数的情况。
我能够在带有 MVE 的 gcc 7.5(与我们的 linux 构建平台相同)上的 godbolt 中重现这个:
#include <iostream>
using namespace std;
struct A
{
A(){}
A(const A&) = delete;
A(A&& other){ cout << "move A " << endl; }
};
template<class T>
struct B
{
template<class U = T>
B(U&& other)
: m_a(std::forward<U>(other))
{
cout << "forward T into B " << endl;
}
T m_a;
};
B<A> foo(A a)
{
return a;
//return std::move(a);
}
int main()
{
A a;
auto b = foo(std::move(a));
}
为了清楚起见,我添加了不可编译的版本来说明问题。
现在,据我所知,在这种情况下我不能使用 (N)RVO,因此移动本身并不是错误的,但我宁愿避免编写 return move(...); 当使用 gcc 8.1(或相对较新的 MSVC)编译它时,它确实使用了没有移动的移动构造函数。那么这只是编译器问题还是我需要改进我的“B”结构来处理这种情况?
解决方法
显示的程序(使用 return a;
)自 C++17 起格式良好。因此,您需要一个符合 C++17 的编译器来编译它。 GCC 7 中的 C++17 支持是实验性的(C++17 尚未发布)。
自 C++11 以来,使用 return std::move(a);
是格式良好的,这应该适用于更旧的编译器。
关于使其在 C++17 中工作的不同措辞:
C++14(草案):
[class.copy]
当满足复制/移动操作的省略条件,但不满足异常声明,并且要复制的对象由左值指定时,或者当 a 中的表达式return 语句是一个(可能用括号括起来的)id 表达式,它命名一个对象,该对象具有在主体或 t (3.1) 的参数声明子句中声明的自动存储持续时间 在最内层的封闭函数或 lambda 表达式中,首先执行重载决议以选择副本的构造函数,就像对象由右值指定一样。
当满足某些条件时,允许实现省略类对象的复制/移动构造......
- 在具有类返回类型的函数中的 return 语句中,当表达式是非易失性自动对象的名称(非函数 或 catch-clause 参数)
C++17(草案):
[class.copy.elision]
隐式可移动实体是一个自动存储持续时间的变量,它可以是非易失性对象或对非易失性对象类型的右值引用。在以下复制初始化上下文中,在尝试复制操作之前首先考虑移动操作:
- 如果 return ([stmt.return]) 或 co_return ([stmt.return.coroutine]) 语句中的表达式是一个(可能带括号的)id 表达式,它命名在主体或参数中声明的隐式可移动实体 -最内层封闭函数或 lambda 表达式的声明子句,或
简而言之,在 return 语句规则中自动移动而不是从左值复制曾经与不适用于函数参数的复制/移动省略规则相结合。现在,它们已经解耦,前者也显式应用于函数参数。