如果我有一个向量(或类似的东西)成员变量,那么move构造函数看起来怎么样

How does the move constructor look like if I have a vector (or anything like it) member variable?

本文关键字:变量 成员 那么 move 怎么样 看起来 构造函数 向量 有一个 如果      更新时间:2023-10-16

标题几乎概括了我的问题。更详细地说:我知道当我在C++11中声明一个移动构造函数和一个移动赋值运算符时,我必须"使其他对象变量为零"。但是,当我的变量不是array或简单的intdouble值,而是更"复杂"的类型时,这是如何工作的呢?

在本例中,我有一个Shoplist类,该类具有vector成员变量。我必须在移动赋值运算符和构造函数中调用vector类的析构函数吗?还是怎样

class Shoplist {
public:
Shoplist() :slist(0) {};
Shoplist(const Shoplist& other) :slist(other.slist) {};
Shoplist(Shoplist&& other) :slist(0) {
slist = other.slist;
other.slist.~vector();
}
Shoplist& operator=(const Shoplist& other);
Shoplist& operator=(Shoplist&& other);

~Shoplist() {};
private:
vector<Item> slist;
};
Shoplist& Shoplist::operator=(const Shoplist& other)
{
slist = other.slist;
return *this;
}
Shoplist& Shoplist::operator=(Shoplist&& other)
{
slist = other.slist;
other.slist.~vector();
return *this;
}

无论std::vector需要做什么才能正确移动,都将由其自己的移动构造函数处理。

因此,假设您想要移动成员,只需直接使用:

Shoplist(Shoplist&& other)
: slist(std::move(other.slist))
{}

Shoplist& Shoplist::operator=(Shoplist&& other)
{
slist = std::move(other.slist);
return *this;
}

在这种情况下,正如AndyG所指出的,只需使用= default让编译器为您生成完全相同的移动ctor和移动赋值运算符。

请注意,像你所做的那样明确地销毁原件肯定是绝对错误的。当other超出范围时,other成员将再次被销毁。


编辑:我确实说过假设你想移动成员,因为在某些情况下你可能不会。

通常,如果数据成员在逻辑上是类的一部分,并且移动起来比复制便宜得多,那么您希望像这样移动数据成员。虽然std::vector的移动肯定比复制便宜,但如果它包含一些暂时缓存或临时值,而这些值在逻辑上不是对象标识或值的一部分,您可能会合理地选择丢弃它。

除非您的类正在管理资源,否则实现复制/移动/析构函数操作是没有意义的。我所说的管理资源是指对其生命周期直接负责:明确的创造和破坏。0的规则和3/5的规则源于这个简单的概念。

你可能会说你的类正在管理slist,但在这种情况下这是错误的:std::vector类直接(正确地)管理与它相关的资源。如果你让我们的类具有隐含的cpy/mv ctos/assignment和dtor,它们将正确地调用相应的std::vector操作。所以你绝对不需要明确地定义它们。在您的情况下,适用0规则。


我知道当我声明移动构造函数和移动赋值时C++11中的运算符我必须"使其他对象变量为零">

不,不是真的。想法是,当你从一个对象中移动时(读作:从对象中移动它的资源),你必须确保你的对象知道它所拥有的资源不再属于它的所有权(例如,这样它就不会试图在它的析构函数中释放它)。在std::vector的情况下,它的move ctor会将指向内部缓冲区的指针设置为nullptr

我知道当我在C++11中声明移动构造函数和移动赋值运算符时,我必须"使其他对象变量为零">

这不太正确。您必须做的是保持从对象中移出的有效性。这意味着您必须满足类不变量。

如果你为一个特定的类指定了一个特殊的不变量,要求你将成员变量设置为零,那么这个类可能必须这样做。但这不是移动的一般要求。


我必须在移动赋值运算符和构造函数中调用向量类的析构函数吗?

绝对不是。当从中移出的对象被销毁时,将调用成员的析构函数。

您通常要做的是移动包含对象的移动构造函数/赋值运算符中的每个成员。这就是隐式生成的特殊成员函数所做的。当然,这可能不满足所有类的类不变量,如果不满足,那么你可能需要编写自己的版本。


如果您不尝试自己声明,编译器将为您隐式生成特殊的成员函数。这是你的类的一个最小但正确的版本:

class Shoplist {
vector<Item> slist;
};

这个类默认是可构造的、可移动的和可复制的。

move构造函数应该按成员顺序移动:

Shoplist(Shoplist&& other)
: slist(std::move(other.slist)) 
{}

请注意,编译器通过成员移动(如果可能)为您生成移动构造函数,就像您在上面手动执行的那样。

Move构造函数被允许(但不需要)"窃取"被移动对象的内容。这并不意味着他们必须"使其他对象变量为零"。例如,移动一个基元类型就相当于复制它。这意味着移动构造函数可以转移堆或自由存储中数据的所有权。在这种情况下,必须修改moved-from对象,这样当它被销毁时(这应该在move构造函数中发生而不是),它以前拥有的数据(在传输之前)将不会被释放。

Vector提供了自己的移动构造函数。因此,要为包含向量的对象编写正确的移动构造函数,所需要做的就是确保调用正确的向量构造函数。这是通过使用std::move:显式地将r值引用传递给子对象构造函数来实现的

Shoplist(Shoplist&& other) :slist(std::move(other.slist)) {
//... Constructor body 

但事实上,一般来说你可能不需要这么做。如果您不声明复制和移动构造函数不声明析构函数,它们将正确地自动生成。(遵循这种做法被称为"0规则"。)

或者,您可以强制编译器自动生成移动构造函数:

Shoplist(Shoplist&& other) = default;