为什么删除移动构造函数导致向量停止工作

时间:2013-03-03 01:22:22

标签: c++ c++11

如果我在一个类中禁止移动构造函数,我就不能再在向量中使用它了:

class Foo
{
  public:
      Foo(int i) : i_(i) {}
      Foo(Foo&&) = delete;

      int i_;
};

int main()
{
    std::vector<Foo> foo;
    foo.push_back(Foo(1));
}

为什么会这样?

2 个答案:

答案 0 :(得分:36)

<强>摘要

不要删除移动成员。


假设您的编译器完全符合C ++ 11,那么显式删除移动构造函数也将隐式地声明以下内容:

Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;

即如果声明一个移动构造函数(或移动赋值运算符),并且不声明复制成员,它们将被隐式声明为已删除。所以你的全班Foo就好像:

class Foo
{
  public:
      Foo(int i) : i_(i) {}
      Foo(Foo&&) = delete;
      Foo(const Foo&) = delete;             // implicitly declared
      Foo& operator=(const Foo&) = delete;  // implicitly declared

      int i_;
};

现在vector<Foo>::push_back(Foo(1))要求FooMoveConstructible。可访问的移动构造函数或甚至可访问的复制构造函数都可以满足MoveConstructible。但是Foo没有。要修复你,可以:

class Foo
{
  public:
      Foo(int i) : i_(i) {}
      Foo(const Foo&) = default;
      Foo& operator=(const Foo&) = default;

      int i_;
};

即。默认复制成员并删除已删除的移动成员。

一般来说,明确删除移动成员并不是一个好主意。如果你想让一个类可以复制而不是“可移动”,那么就像在C ++ 03中一样声明:声明/定义你的复制成员。您可以使用= default编译生成复制成员,并且仍然将其视为用户声明。并且不要声明移动成员。不存在的移动成员与已删除的移动成员不同。

已删除的移动成员意味着您无法从右侧构造Foo的副本,即使复制构造函数可以正常工作也是如此。这很少是预期的意图。

即使您希望您的课程不可复制也不可移动,最好只删除复制成员并保持移动成员未声明(意味着它们不存在)。如果您正在查看代码(包括您自己的代码),并且看到已删除的移动成员,那么它们几乎肯定是不正确的,或者是最好的多余和混乱。

有一天,有人会为删除的移动成员提供一个好的用例。但这将是一个罕见的用例。如果您在代码中看到这样的模式,您应该期望代码作者有一个非常好的解释。否则,删除的移动成员可能只是不正确(充其量是多余的)。但从好的方面来看,这个错误会在编译时显示,而不是在运行时(如你的例子中所示)。

以下是当您明确声明任何特殊成员时编译器将隐式执行的操作的摘要图表。那些红色的方块表示弃用行为。

enter image description here

= default= delete计为用户声明的

Click here如果你想查看完整的幻灯片。

答案 1 :(得分:0)

你说:

  

我不能再在矢量中使用它了:

然而,自从C ++ 11以来,在向量中使用的要求很少;相反,每个向量操作都有自己的要求。因此,实际上您可以在向量中使用Foo,但仅限于不可能要求移动或复制对象的操作。

例如,你可以写:

std::vector<Foo> w(5);
w.pop_back();

您也可以拨打w[0].some_member_function();等等。

但是,由于您定义Foo

的方式,您无法编写以下任何内容
std::vector<Foo> w = { 1, 2, 3 };   // Requires moving elements out of initializer_list
w.resize(5);        // Resize may mean reallocation which may require elements to be moved to new location
w.push_back(5);     // ditto
w.erase(w.begin()); // Erasing an element may require other elements to move to fill its place
w[0] = 1;           // operator= is implicitly deleted as described in Howard's answer (but you could explicitly default it to make this work)

如果你想让Foo可以复制,但不能移动 - 而且任何移动场景都会回归到使用副本 - 那么唯一的方法就是不要声明任何移动构造函数一点都没通过声明析构函数,复制构造函数和/或复制赋值运算符来强制禁止编译器生成的move-constructor,如Howard的表中所示。

请参阅Howard的答案中的最后一个代码示例。

要清楚的是,移动构造函数实际上有几种不同的可能状态,而不是所有在Howard表中显示的状态:

  1. 定义为已删除和用户自定义
  2. 定义为已删除,而非用户定义(当成员不可移动时会发生这种情况)
  3. 默认和用户定义的
  4. 默认而非用户定义
  5. 用户提供(即用户定义,既不默认也不删除)
  6. 未声明
  7. 在上面的情况1,3,4,5中,通过重载解析找到移动构造函数。在情况2和6中,过载分辨率没有发现;并且push_back(Foo(1));等复制/移动方案将回退到复制构造函数(如果有)。