使用 move-constructor 时将 self 重置为 nullptr 是一个好习惯吗?
Is it a good habit to reset self to nullptr when use move-constructor?
在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
将再次检查您的指针。delete
nullptr
是完全安全的。不要打扰。
非常糟糕的代码:
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;
}
如此处所定义
- 在解决链表问题时创建一个额外的节点是一个好习惯吗?
- 使用完数据结构后清空数据结构是一个好习惯吗?
- 如何为位集找到/实现一个好的哈希函数
- 让二传手返回布尔值是好习惯吗?
- 一个好的线程池队列大小
- 在同一C 源文件中使用多个名称空间是一个好习惯吗?
- 是聪明的指针是RAII的好习惯
- 使用 move-constructor 时将 self 重置为 nullptr 是一个好习惯吗?
- 这是 c++ 中一个好的 goto 语句吗?
- 常量参考延长对象的寿命,然后是const_cast,这是一个好主意吗?
- 将指针施放为成员函数作为C函数的指针是一个好习惯吗?
- 将C 11设置功能更改为带有转发的现代模板功能是一个好主意
- 重建operator()是重建的好习惯
- 创建多个线程以处理多个请求是一个好练习吗?
- 如何为我的班级创建一个好的界面
- std :: tr1 :: shared_ptr throw bad_alloc,也是一个好主意
- 在cccallfuncnd中,我们通过指针.通过当地范围的指针是一个好习惯
- C++使用"this"是一个好习惯吗?
- 有没有一个好的习惯用法来处理替代输出流
- 在合适的地方,在成员函数的末尾添加const是不是一个好习惯?