我无法弄清楚为什么在最后一种情况下启用了复制省略时调用的移动构造函数(甚至是强制性的,例如在C ++ 17中):
class X {
public:
X(int i) { std::clog << "converting\n"; }
X(const X &) { std::clog << "copy\n"; }
X(X &&) { std::clog << "move\n"; }
};
template <typename T>
X make_X(T&& arg) {
return X(std::forward<T>(arg));
}
int main() {
auto x1 = make_X(1); // 1x converting ctor invoked
auto x2 = X(X(1)); // 1x converting ctor invoked
auto x3 = make_X(X(1)); // 1x converting and 1x move ctor invoked
}
在这种情况下,哪些规则会妨碍移动构造函数被忽略?
更新
调用移动构造函数时可能会有更直接的情况:
X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
答案 0 :(得分:17)
这两种情况略有不同,理解其中的原因非常重要。使用C ++ 17中的新值语义,基本思想是我们尽可能地延迟将prvalues转换为对象的过程。
template <typename T>
X make_X(T&& arg) {
return X(std::forward<T>(arg));
}
int main() {
auto x1 = make_X(1);
auto x2 = X(X(1));
auto x3 = make_X(X(1));
}
对于x1
,我们第一个类型X
的表达式是make_X
正文中的表达式,基本上是return X(1)
。这是X
类型的prvalue。我们正在使用该prvalue初始化make_X
的返回对象,然后make_X(1)
本身就是X
类型的prvalue,因此我们将延迟实现。从T
类型的prvalue初始化T
类型的对象意味着直接初始化from the initializer,因此auto x1 = make_X(1)
缩减为X x1(1)
。
对于x2
,减少甚至更简单,我们只是直接应用规则。
对于x3
,情况不同。我们有一个类型X
早期(X(1)
参数)的prvalue,并且prvalue绑定到引用!在绑定时,我们应用the temporary materialization conversion - 这意味着我们实际创建a temporary object。 然后将对象移动到返回对象中,我们可以一直对后续表达式进行prvalue减少。所以这基本上减少了:
X __tmp(1);
X x3(std::move(__tmp));
我们仍有一个动作,但只有一个动作(我们可以避免链式动作)。它是对引用的绑定,需要存在单独的X
对象。参数arg
和make_X
的返回对象必须是不同的对象 - 这意味着必须进行移动。
最后两个案例:
X x4 = std::forward<X>(X(1));
X x5 = static_cast<X&&>(X(1));
在这两种情况下,我们都会绑定对prvalue的引用,这又需要进行临时实现转换。然后在这两种情况下,初始化器都是x值,所以我们不能减少prvalue - 我们只是从xvalue移动构造,它是来自prvalue的物化临时对象。
答案 1 :(得分:5)
因为在表达式X(std::forward<T>(arg))
中,即使在最后一种情况下,arg
是绑定到临时的引用,它仍然不是临时的。在函数体内,编译器无法确保arg
未绑定到左值。考虑如果移动构造函数被删除会发生什么,并且您将执行此调用:
auto x4 = make_X(std::move(x2));
x4
将成为x2
的别名。
[...]在下列情况下(可以合并以消除多份副本),允许复制/移动操作的省略,称为复制省略:
在具有类返回类型的函数的return语句中,当表达式是具有相同cv-unqualified类型的非易失性自动对象(函数或catch子句参数除外)的名称时作为函数返回类型,可以通过将自动对象直接构造为函数的返回值来省略复制/移动操作
- 的目标中而省略
当一个未绑定到引用([class.temporary])的临时类对象被复制/移动到具有相同cv-nonqualified类型的类对象时,复制/移动操作可以是通过将临时对象直接构造到省略的副本/移动
在电话make_X(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));
Nota:在我看到关于C ++ 17的新答案后,我想加入混乱。
在C ++ 17中,prvalue
的定义发生了变化,即在示例代码中没有任何移动构造函数可以删除。这里是GCC的result code示例,在C ++ 14中使用选项fno-elide-constructors
,然后在C ++ 17中:
#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
答案 2 :(得分:4)
简化您的示例:
auto x1 = make_X(1); // converting
auto x2 = X(X(1)); // converting
auto x4 = X(std::forward<X>(X(1))); // converting + move
来自cppreference的copy elision documentation(强调我的):
在c ++ 17之前:
在下列情况下,编译器是允许的,但是 不要求省略复制和移动 - (自C ++ 11)构造 类对象......
- 如果函数按值返回类类型,则返回 statement&table;表达式是非易失性对象的名称 自动存储持续时间,不是功能参数,或者是 catch子句参数,它具有相同的类型(忽略 顶级cv-qualification)作为函数的返回类型,然后 复制/移动(因为C ++ 11)被省略。当那个本地对象是 建造后,它直接在仓库中建造 否则,函数的返回值将被移动或复制到。这个 复制省略的变体称为NRVO,&#34;命名为返回值 优化&#34;
从c ++ 17开始:
在下列情况下,编制者需要省略 复制和移动构建......
a)在初始化中,如果初始化表达式是prvalue而且是 cv-源类型的非限定版本与该类相同 目的地的类,初始化表达式用于 初始化目标对象:
T x = T(T(T())); // only one call to default constructor of T, to initialize x
b)在函数调用中,如果 return语句的操作数是 prvalue 并且函数的返回类型与其类型相同 prvalue。
T f() { return T{}; } T x = f(); // only one call to default constructor of T, to initialize x T* p = new T(f()); // only one call to default constructor of T, to initialize *p
在任何情况下std::forward
都不符合要求,因为它的结果是 xvalue ,而不是 prvalue :它不会返回类按值键入。因此,不会出现任何缺陷。