使用 move-constructor 时将 self 重置为 nullptr 是一个好习惯吗?

Is it a good habit to reset self to nullptr when use move-constructor?

本文关键字:一个 好习惯 self 时将 move-constructor nullptr 使用      更新时间:2023-10-16

在C++11中,移动构造函数/运算符支持资源/内存移动。

这是我的例子:

class A {
public:
A() : table_(nullptr), alloc_(0) {}
~A()
{
if (table_)
delete[] table_;
}
A(const A & other)
{
// table_ is not initialized
// if (table_)
//    delete[] table_;
table_ = new int[other.alloc_];
memcpy(table_, other.table_, other.alloc_ * sizeof(int));
alloc_ = other.alloc_;
}
A& operator=(const A & other)
{
if (table_)
delete[] table_;
table_ = new int[other.alloc_];
memcpy(table_, other.table_, other.alloc_ * sizeof(int));
alloc_ = other.alloc_;
return *this;
}
A(A && other)
{
// table_ is not initialized in constructor
// if (table_)
//    delete[] table_;
table_ = other.table_;
alloc_ = other.alloc_;
}
A& operator=(A && other)
{
if (table_)
delete[] table_;
table_ = other.table_;
alloc_ = other.alloc_;
}
private:
int *table_;
int alloc_;
};

看起来不错,但有时我想移动一个局部变量,如下所示:

class B {
private:
A a_;
public:
void hello()
{
A tmp;
// do something to tmp
a_ = std::move(tmp);
// tmp.~A() is called, so a_ is invalid now.
}
};

当函数结束时,将调用tmp.~A(),此时,a_tmp具有相同的table_指针,当tmp delete[] table_时,a_'s table_将无效。

我在徘徊什么时候应该使用std::move将 tmp 分配给a_,而无需复制。

在答案的帮助下,我像这样修改 A 的移动构造函数:

class A {
private:
void reset()
{
table_ = nullptr;
alloc_ = 0;
}
public:
A(A && other)
{
table_ = other.table_;
alloc_ = other.alloc_;
other.reset();
}
A& operator=(A && other)
{
std::swap(table_, other.table_);
std::swap(alloc_, other.alloc_);
}
};

在这个代码中,当我移动一些东西时,我会交换新旧引用,所以旧的tmp会删除[]原始a_ table_,这是无用的。

这样做是个好习惯。

当你在A(A && other)中从other移动时,你还应该将其移动的数据成员设置为 nulltpr。因此,固定代码应如下所示:

A(A && other)
{
//if (table_)
//    delete[] table_; // no need for this in move c-tor
table_ = other.table_;
other.table_ = nullptr;
alloc_ = other.alloc_;
other.alloc_ = nullptr;
}
A& operator=(A && other)
{
// as n.m. has pointed out, this move assignment does not 
// protect against self assignment. One solution is to use
// swap aproach here. The other is to simply check if table_ == other.table_. 
// Also see here for drawbacks of swap method:
// http://scottmeyers.blogspot.com/2014/06/the-drawbacks-of-implementing-move.html
delete[] table_;
table_ = other.table_;
other.table_ = nullptr;
alloc_ = other.alloc_;
other.alloc_ = nullptr;
return *this;
}

这使other标准调用valid but unspecified state

您也可以按如下方式使用 std::swap:

A(A && other)
{
table_ = other.table_;
alloc_ = other.alloc_;
}
A& operator=(A && other)
{
std::swap(table_, other.table_);
std::swap(alloc_, other.alloc_);
return *this;
}

这样,当 movefrom 对象被销毁时,将完成释放。

这段代码有很多问题(即使假设你确实想摆弄数组和指针,这在现实生活中是不应该的。只需使用 std::vector)。

错误代码:

A()
{
table_ = nullptr;
alloc_ = 0;
}

不要在 ctor 正文中使用赋值,使用成员初始化列表。好代码:

A() : table{nullptr}, alloc_ {0} {}

其他构造函数也是如此。


冗余代码:

if (table_)
delete[] table_;

delete将再次检查您的指针。deletenullptr是完全安全的。不要打扰。


非常糟糕的代码:

A(const A & other)
{
if (table_)
delete[] table_;

table未初始化。访问它是 UB。此外,无需在构造函数中进行此检查。新构造的对象中不会有任何分配。只需删除支票即可。其他构造函数也是如此。


错误代码:

A& operator=(const A & other)
{
if (table_)
delete[] table_;

不能防止自我分配。其他赋值运算符也是如此。


这些都是需要忘记的习惯,无论你是C++03还是C++11编码。现在进行移动:

A(A && other)
{
if (table_)
delete[] table_;
table_ = other.table_;
alloc_ = other.alloc_;
}

这是完全错误的。您需要更改要移动的对象,否则它根本不是移动,而是简单的浅层副本。

A(A && other) : table_{other.table_}, alloc_{other.alloc_} {
{
other.table_ = nullptr;
other.alloc_ = 0;        
}

移动分配也是如此。


移动中的std::swap在处理用户定义类型时,CTOR 是一个很好的习语。基元类型并不完全需要它,主要是因为您需要先初始化它们,然后立即交换,但您仍然可以使用它。

移动构造函数和赋值运算符正在有效地执行浅拷贝。您应该将other.table设置为nullptr,以便在这种情况下移动有意义。当然,这将避免两次删除同一数组的未定义行为,正如您在示例中建议的那样。

一个不错的选择是交换移动构造函数中的值。

A& operator=(A && other)
{
using namespace std;
swap(table_, other.table_);
swap(alloc_, other.alloc_);
return *this;
}

这样,源的内容被放置在目标中,后者的内容被转移到源中 - 然后它会在删除本身时正确清理它们(无论如何,这是你所期望的,否则,你不会想要移动对象......

移动构造函数可以从上面的赋值中获利,然后:

A(A&& other) : A()
{
*this = std::move(other);
}

在移动构造函数/赋值中,复制指针后,将它们赋值"nullptr",这样当析构函数被调用时,它将不是操作。

这就是我如何编写移动构造函数和赋值。而且,您可以避免"if"检查"删除",如果是"nullptr",则不是op。

A(A && other)
{
delete[] table_;
table_ = other.table_;
other.table_ = nullptr;
alloc_ = other.alloc_;
}
A& operator=(A && other) {
delete[] table_;
table_ = other.table_;
other.table_ = nullptr; // assign the source to be nullptr
alloc_ = other.alloc_;
return *this;
}

我认为您需要像这样更改移动构造函数赋值运算符

// Simple move constructor
A(A&& arg) : member(std::move(arg.member)) // the expression "arg.member" is lvalue
{} 
// Simple move assignment operator
A& operator=(A&& other) {
member = std::move(other.member);
return *this;
}

如此处所定义