如果我在一个类中禁止移动构造函数,我不能再在向量中使用它:
class Foo
{
public:
Foo(int i) : i_(i) {}
Foo(Foo&&) = delete;
int i_;
};
int main()
{
std::vector<Foo> foo;
foo.push_back(Foo(1));
}
为什么会这样?
概括
不要删除移动成员。
假设您的编译器完全符合 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))
要求Foo
是MoveConstructible
。 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
从右值构造副本,即使复制构造函数可以正常工作。这很少是期望的意图。
即使您希望您的类不可复制或移动,最好只删除复制成员并保留未声明的移动成员(这意味着它们将不存在)。如果你曾经审查过代码(包括你自己的),并且看到删除的 move 成员,它们几乎肯定是不正确的,或者最好是多余的和令人困惑的。
有一天,有人会为已删除的移动成员提出一个很好的用例。但这将是一个罕见的用例。如果您在代码中看到这样的模式,您应该期望代码作者有一个很好的解释。否则,删除的移动成员可能只是不正确的(充其量是多余的)。但从好的方面来说,这个错误会在编译时显示出来,而不是在运行时(如你的例子中)。
这是当您显式声明任何特殊成员时编译器将隐式执行的摘要图表。那些红色的方块代表不推荐使用的行为。
= default
并= delete
算作user-declared。
如果您想查看完整的幻灯片,请单击此处。
你说:
我不能再在向量中使用它:
但是,从 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、3、4、5 中,移动构造函数是通过重载决议找到的。在情况 2 和 6 中,重载决议找不到;和复制/移动场景,例如push_back(Foo(1));
将回退到复制构造函数(如果有的话)。