12

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

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

      int i_;
};

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

为什么会这样?

4

2 回答 2

39

概括

不要删除移动成员。


假设您的编译器完全符合 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))要求FooMoveConstructibleMoveConstructible可以通过可访问的移动构造函数,甚至可以通过可访问的复制构造函数来满足。但Foo两者都没有。要修复你可以:

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

      int i_;
};

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

一般来说,显式删除移动成员不是一个好主意。如果您希望一个类是可复制的但不是“可移动的”,只需像在 C++03 中一样声明:声明/定义您的复制成员。您可以让复制成员使用 编译器生成= default,这仍然算作用户声明。并且不要声明移动成员。不存在的移动成员与已删除的移动成员不同。

删除的移动成员意味着您不能Foo从右值构造副本,即使复制构造函数可以正常工作。这很少是期望的意图。

即使您希望您的类不可复制或移动,最好只删除复制成员并保留未声明的移动成员(这意味着它们将不存在)。如果你曾经审查过代码(包括你自己的),并且看到删除的 move 成员,它们几乎肯定是不正确的,或者最好是多余的和令人困惑的。

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

这是当您显式声明任何特殊成员时编译器将隐式执行的摘要图表。那些红色的方块代表不推荐使用的行为。

在此处输入图像描述

= default= delete算作user-declared

如果您想查看完整的幻灯片,请单击此处。

于 2013-03-03T01:58:49.217 回答
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可复制但不可移动——并且任何移动场景都回退到使用副本——那么做到这一点的唯一方法就是根本不声明任何移动构造函数;通过声明析构函数、复制构造函数和/或复制赋值运算符来强制禁止编译器生成的移动构造函数,如 Howard 的表中所示。

有关此示例,请参阅霍华德答案中的最后一个代码示例。

需要明确的是,移动构造函数实际上有几种不同的可能状态,霍华德的表格中并未全部显示:

  1. 定义为已删除,用户自定义
  2. 定义为已删除,而不是用户定义(当成员不可移动时会发生这种情况)
  3. 默认和用户定义
  4. 默认且非用户定义
  5. 用户提供(即用户定义,既不默认也不删除)
  6. 未申报

在上述情况 1、3、4、5 中,移动构造函数是通过重载决议找到的。在情况 2 和 6 中,重载决议找不到;和复制/移动场景,例如push_back(Foo(1));将回退到复制构造函数(如果有的话)。

于 2016-03-04T00:47:06.060 回答