因为在表达式X(std::forward<T>(arg))
中,即使在最后一种情况下,arg
是一个绑定到临时对象的引用,它仍然不是一个临时对象。在函数体内部,编译器无法确保arg
没有绑定到一个左值。考虑如果移动构造函数被省略并且您执行了这个调用会发生什么:
auto x4 = make_X(std::move(x2));
x4
将成为x2
的别名。
返回值移动省略的规则在[class.copy]/32中描述:
[...]这种称为复制省略的复制/移动操作可以在以下情况下允许(可以组合以消除多个副本):
在具有类返回类型的函数中的返回语句中,当表达式是与函数返回类型具有相同cv-unqualified类型的非易失性自动对象(而不是函数或catch-clause参数)的名称时,可以通过直接构造自动对象到函数的返回值中来省略复制/移动操作
当一个未绑定到引用([class.temporary])的临时类对象将被复制/移动到具有相同cv-unqualified类型的类对象时,可以通过直接构造临时对象到省略的复制/移动的目标中来省略复制/移动操作
在调用make_X(X(1))
时,实际上发生了复制省略,但只发生一次:
- 首先,X(1)创建一个临时对象,该对象绑定到
arg
。
- 然后,
X(std::forward<T>(arg))
调用移动构造函数。arg
不是临时对象,因此上述第二条规则不适用。
- 然后,表达式
X(std::forward<T>(arg))
的结果也应该被移动以构造返回值,但是这个移动被省略了。
关于您的更新,std::forward
会导致绑定到xvalue的临时X(1)
实例化:即std::forward
的返回值。这个返回的xvalue不再是一个临时对象,所以复制/省略不再适用。
如果移动省略发生在这种情况下会发生什么(c++语法不具有上下文):
auto x7 = std::forward<X>(std::move(x2))
注:看到有关C++17的新答案后,我想增加一些混淆。
在C++17中,“prvalue”的定义已更改,因此您的示例代码中不再有任何移动构造函数可省略。这里是使用C++14和C++17选项“fno-elide-constructors”进行编译的GCC的结果代码示例:
#c++ -std=c++14 -fno-elide-constructors | #c++ -std=c++17 -fno-elide-constructors
main: | main:
sub rsp, 24 | sub rsp, 24
mov esi, 1 | mov esi, 1
lea rdi, [rsp+15] | lea rdi, [rsp+12]
call X::X(int) | call X::X(int)
lea rsi, [rsp+15] | lea rdi, [rsp+13]
lea rdi, [rsp+14] | mov esi, 1
call X::X(X&&) | call X::X(int)
lea rsi, [rsp+14] | lea rdi, [rsp+15]
lea rdi, [rsp+11] | mov esi, 1
call X::X(X&&) | call X::X(int)
lea rdi, [rsp+14] | lea rsi, [rsp+15]
mov esi, 1 | lea rdi, [rsp+14]
call X::X(int) | call X::X(X&&)
lea rsi, [rsp+14] | xor eax, eax
lea rdi, [rsp+15] | add rsp, 24
call X::X(X&&) | ret
lea rsi, [rsp+15]
lea rdi, [rsp+12]
call X::X(X&&)
lea rdi, [rsp+13]
mov esi, 1
call X::X(int)
lea rsi, [rsp+13]
lea rdi, [rsp+15]
call X::X(X&&)
lea rsi, [rsp+15]
lea rdi, [rsp+14]
call X::X(X&&)
lea rsi, [rsp+14]
lea rdi, [rsp+15]
call X::X(X&&)
xor eax, eax
add rsp, 24
ret
auto x2 = X(X(1));
我认为不应该调用移动构造函数甚至需要它的存在。 - Daniel Langr