当类显式声明复制操作(即复制构造函数或复制赋值运算符)时,不会为该类声明移动操作。但是当一个类显式声明一个移动操作时,复制操作被声明为已删除。为什么存在这种不对称?为什么不指定如果声明移动操作,则不会声明复制操作?据我所知,不存在任何行为差异,也不需要对移动和复制操作进行不对称处理。
[对于喜欢引用该标准的人,在12.8 / 9和12.8 / 20中指定缺少具有复制操作声明的类的移动操作声明,并且指定了具有移动操作声明的类的已删除复制操作在12.8 / 7和12.8 / 18。]
答案 0 :(得分:7)
当一个类被移动但是由于没有声明移动构造函数时,编译器会回退到复制构造函数。在相同的情况下,如果将移动构造函数声明为已删除,则程序将是格式错误的。因此,如果将移动构造函数隐式声明为已删除,则许多涉及现有C ++ 11之前类的合理代码将无法编译。像myVector.push_back(MyClass())
这解释了为什么在定义复制构造函数时不能隐式声明移动构造函数。这就留下了为什么在定义移动构造函数时隐式声明复制构造函数被删除的问题。
我不知道委员会的确切动机,但我有猜测。如果将移动构造函数添加到现有的C ++ 03样式类中,则删除(先前隐式定义的)复制构造函数,那么使用此类的现有代码可能会以微妙的方式改变含义,因为重载决策选择了过去的意外重载被拒绝作为更糟糕的比赛。
考虑:
struct C {
C(int) {}
operator int() { return 42; }
};
C a(1);
C b(a); // (1)
这是一个遗留的C ++ 03类。 (1)调用(隐式定义的)复制构造函数。 C b((int)a);
也是可行的,但是比较差。
想象一下,无论出于何种原因,我决定在这个类中添加一个显式移动构造函数。如果移动构造函数的存在是为了抑制复制构造函数的隐式声明,那么(1)处看似无关的代码片段仍然会编译,但是会默默地改变它的含义:它现在会调用operator int()
和{{1 }}。那会很糟糕。
另一方面,如果复制构造函数被隐式声明为已删除,则(1)将无法编译,提醒我该问题。我会检查一下情况,然后决定是否还需要一个默认的复制构造函数;如果是这样,我会添加C(int)
答案 1 :(得分:2)
为什么存在这种不对称?
向后兼容性,因为复制和移动之间的关系已经不对称。 MoveConstructible的定义是CopyConstructible的一个特例,这意味着所有CopyConstructible类型也是MoveConstructible类型。这是真的,因为采用引用到const的复制构造函数将处理rvalues和lvalues。
可以从没有移动构造函数的rvalues初始化可复制类型(它可能不像移动构造函数那样高效)。
复制构造函数还可用于在移动基础子对象时在派生类的隐式定义的移动构造函数中执行“移动”。
因此,复制构造函数可以被视为“退化移动构造函数”,因此如果某个类型具有复制构造函数,它并不严格需要移动构造函数,它已经是MoveConstructible,所以简单没有声明移动构造函数是可以接受的。
相反的情况并非如此,可移动类型不一定是可复制的,例如,仅移动类型。在这些情况下,复制构造函数和赋值被删除提供了更好的诊断,而不仅仅是没有声明它们并且得到关于将左值绑定到右值引用的错误。
为什么不指定如果声明移动操作,则不会声明复制操作?
更好的诊断和更明确的语义。 “定义为已删除”是C ++ 11清楚地说“不允许此操作”的方式,而不是因为其他原因而被误删或丢失。
移动构造函数和移动赋值运算符的“未声明”的特殊情况是不寻常的,并且由于上述不对称性而特殊,但特殊情况通常最好保留在一些狭窄的情况下(这里值得注意的是“不是声明“也可以应用于默认构造函数。”
另外值得注意的是,您引用的一个段落[class.copy] p7表示(强调我的):
如果类定义没有显式声明复制构造函数,则会声明隐式。如果类定义声明了移动构造函数或移动赋值运算符,则隐式声明的复制构造函数被定义为已删除;否则,它被定义为默认值(8.4)。 如果类具有用户声明的复制赋值运算符或用户声明的析构函数,则不推荐使用后一种情况。
“后一种情况”是指“否则,它被定义为默认”部分。第18段对副本分配算子有类似的措辞。
因此委员会的意图是在C ++的某些未来版本中,其他类型的特殊成员函数也会导致复制构造函数和复制赋值运算符被删除。原因是如果你的类需要一个用户定义的析构函数,那么隐式定义的复制行为可能不会做正确的事情。由于向后兼容性原因,尚未针对C ++ 11或C ++ 14进行此更改,但我们的想法是,在某些未来版本中,为了防止复制构造函数和复制赋值运算符被删除,您需要明确声明它们并且将它们定义为默认值。
因此,删除复制构造函数(如果它们可能做不正确的事情)是一般情况,而“未声明”只是移动构造函数的特例,因为复制构造函数无论如何都可以提供简并移动。
答案 2 :(得分:0)
本质上是为了避免迁移的代码执行意外的不同操作。
复制和移动需要一定程度的连贯性,所以C ++ 11 - 如果你只声明一个 - 压制另一个。
考虑一下:
C a(1); //init
C b(a); //copy
C c(C(1)); //copy from temporary (03) or move(11).
假设您在C ++ 03中编写此内容。
假设我稍后在C ++ 11中编译它。 如果没有声明ctor,则默认移动会复制一个副本(因此最终行为与C ++ 03相同)。
如果声明了copy,则删除move,并且正弦C&&
衰减到C const&
第三个语句将导致临时复制。这仍然是C ++ 03相同的行为。
现在,如果我稍后添加一个移动ctor,这意味着我正在改变C的行为(在C ++ 03中定义C时你没有计划的东西),并且因为可移动对象不需要可复制(反之亦然),编译器假定通过使其可移动,dafault副本可能不再适用。我应该与移动一致地实施它,或者如果我发现它足够 - 恢复C(const C&)=default;