3 回答
这称为复制省略。
这种情况下的规则是指定了复制/移动操作,但允许编译器选择性地忽略它作为优化,即使复制/移动构造函数有副作用。
当发生复制省略时,通常直接在目标的内存空间中创建对象;而不是创建一个新对象,然后将其复制/移动到目的地并删除第一个对象。
复制/移动构造函数仍然必须实际存在,否则我们最终会遇到愚蠢的情况,即代码似乎可以编译,但是当编译器决定不进行复制省略时编译失败。或者代码可以在某些编译器上运行并在其他编译器上中断,或者如果您使用不同的编译器开关。
在您的第一个示例中,您没有声明副本或移动构造函数。这意味着它获得了一个隐式定义的复制构造函数。
但是,有一个规则,如果一个类有一个用户定义的析构函数,那么它不会得到一个隐式定义的移动构造函数。不要问我为什么存在这条规则,但它确实存在(参见 [class.copy]#9 以供参考)。
现在,标准的确切措辞在这里很重要。在 [class.copy]#13 中它说:
默认且未定义为已删除的复制/移动构造函数如果被odr-used (3.2)则隐式定义
[注意:复制/移动构造函数是隐式定义的,即使实现省略了它的 odr-use (3.2, 12.2)。——尾注
odr-used的定义相当复杂,但它的要点是,如果您从不尝试复制对象,那么它将不会尝试生成隐式定义的复制构造函数(同样用于移动和移动)。
正如 TC 在您之前的线程中所解释的那样,所做的行为A a[2] = {0, 1};
确实指定了一个复制/移动,即该值a[0]
必须通过复制或移动从一个临时的A(0)
. 这个临时文件能够进行复制省略,但正如我之前解释的那样,正确的构造函数必须仍然存在,这样如果编译器决定在这种情况下不使用复制省略,代码才能工作。
由于您的类在此处没有移动构造函数,因此无法移动它。但是尝试将临时绑定到构造函数的尝试A
仍然成功,因为定义了一个复制构造函数(尽管是隐式定义的)。此时,发生odr-use并尝试生成复制构造函数,但由于unique_ptr
.
在您的第二个示例中,您提供了一个移动构造函数,但没有提供复制构造函数。仍然有一个隐式声明的复制构造函数,直到它像以前一样被odr-used才生成。
但是重载决议的规则说,如果复制和移动都可能,则使用移动构造函数。所以在这种情况下它不会使用复制构造函数,一切都很好。
在第三个示例中,移动构造函数再次赢得重载决议,因此复制构造函数的定义方式无关紧要。
我想你在问为什么
A a[2] = { 0, 1 };
无法编译,而您希望它能够编译,因为A
可能有一个移动构造函数。但事实并非如此。
原因是它A
有一个不可复制的成员,所以它自己的复制构造函数是有一个用户声明的析构函数。deleted
,这算作用户声明的复制构造函数
这反过来意味着A
没有隐式声明的移动构造函数。您必须启用移动构造,您可以通过默认构造函数来执行此操作:
A(A&&) = default;
要检查一个类是否可移动构造,您可以使用标题is_move_constructible
中的 , :type_traits
std::cout << std::boolalpha;
std::cout << std::is_move_constructible<A>::value << std::endl;
这false
在你的情况下输出。
扭曲的逻辑是您应该在更高的抽象级别编写程序。如果一个对象有一个复制构造函数,它可以被复制,否则它不能。如果你告诉编译器这个对象不应该被复制,它会服从你而不是作弊。一旦你告诉它它可以被复制,编译器将尝试尽可能快地进行复制,通常是通过避免复制构造函数。
至于移动构造函数:它是一种优化。将一个对象从一个地方移动到另一个地方往往比制作一个精确的副本并销毁旧的更快。这就是移动构造函数的用途。如果没有移动构造函数,移动仍然可以使用老式的复制和销毁方法完成。