我看到许多代码在复制和交换方面实现了五条规则,但我认为我们可以使用移动函数来替换交换函数,如下面的代码所示:
#include <algorithm>
#include <cstddef>
class DumbArray {
public:
DumbArray(std::size_t size = 0)
: size_(size), array_(size_ ? new int[size_]() : nullptr) {
}
DumbArray(const DumbArray& that)
: size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
std::copy(that.array_, that.array_ + size_, array_);
}
DumbArray(DumbArray&& that) : DumbArray() {
move_to_this(that);
}
~DumbArray() {
delete [] array_;
}
DumbArray& operator=(DumbArray that) {
move_to_this(that);
return *this;
}
private:
void move_to_this(DumbArray &that) {
delete [] array_;
array_ = that.array_;
size_ = that.size_;
that.array_ = nullptr;
that.size_ = 0;
}
private:
std::size_t size_;
int* array_;
};
我想这个代码
- 例外安全
- 需要更少的输入,因为许多函数只调用move_to_this(),并且复制赋值和移动赋值在一个函数中统一
- 比复制和交换更有效,因为交换涉及3个分配,而这里只有2个,并且此代码不会遇到上述问题 这个链接
我对吗?
谢谢
编辑:
- 正如@Leon指出的那样,可能需要一个用于释放资源的专用函数,以避免代码重复
move_to_this()
和析构函数
正如@thorsan指出的那样,对于极端的性能问题,最好分开 DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; }
成 DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); return *this; }
(感谢@MikeMB)和 DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; }
避免额外的移动操作
添加一些调试打印后,我发现没有涉及额外的动作 DumbArray& operator=(DumbArray that) {}
当你把它称为移动任务时
正如@ErikAlapää指出的那样,之前需要进行自我指派检查 delete
在 move_to_this()
评论内联,但简要说明:
你想要所有的移动任务和移动构造函数 noexcept
如果可能的话。标准库是 许多 如果启用此选项会更快,因为它可以从重新排序对象序列的算法中删除任何异常处理。
如果您要定义自定义析构函数,请将其设为noexcept。为什么打开潘多拉的盒子? 我错了。默认情况下它是noexcept。
在这种情况下,提供强大的异常保证是无痛的,几乎没有任何成本,所以让我们这样做。
码:
#include <algorithm>
#include <cstddef>
class DumbArray {
public:
DumbArray(std::size_t size = 0)
: size_(size), array_(size_ ? new int[size_]() : nullptr) {
}
DumbArray(const DumbArray& that)
: size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
std::copy(that.array_, that.array_ + size_, array_);
}
// the move constructor becomes the heart of all move operations.
// note that it is noexcept - this means our object will behave well
// when contained by a std:: container
DumbArray(DumbArray&& that) noexcept
: size_(that.size_)
, array_(that.array_)
{
that.size_ = 0;
that.array_ = nullptr;
}
// noexcept, otherwise all kinds of nasty things can happen
~DumbArray() // noexcept - this is implied.
{
delete [] array_;
}
// I see that you were doing by re-using the assignment operator
// for copy-assignment and move-assignment but unfortunately
// that was preventing us from making the move-assignment operator
// noexcept (see later)
DumbArray& operator=(const DumbArray& that)
{
// copy-swap idiom provides strong exception guarantee for no cost
DumbArray(that).swap(*this);
return *this;
}
// move-assignment is now noexcept (because move-constructor is noexcept
// and swap is noexcept) This makes vector manipulations of DumbArray
// many orders of magnitude faster than they would otherwise be
// (e.g. insert, partition, sort, etc)
DumbArray& operator=(DumbArray&& that) noexcept {
DumbArray(std::move(that)).swap(*this);
return *this;
}
// provide a noexcept swap. It's the heart of all move and copy ops
// and again, providing it helps std containers and algorithms
// to be efficient. Standard idioms exist because they work.
void swap(DumbArray& that) noexcept {
std::swap(size_, that.size_);
std::swap(array_, that.array_);
}
private:
std::size_t size_;
int* array_;
};
在移动赋值运算符中可以进一步提高性能。
我提供的解决方案提供了一个保证,即移动的数组将为空(资源被释放)。这可能不是你想要的。例如,如果你单独跟踪DumbArray的容量和大小(例如,像std :: vector),那么你可能想要任何已分配的内存 this
留在 that
搬家后。这将允许 that
分配给可能在没有其他内存分配的情况下离开。
为了实现这种优化,我们只需根据(noexcept)swap实现move-assign运算符:
所以从这个:
/// @pre that must be in a valid state
/// @post that is guaranteed to be empty() and not allocated()
///
DumbArray& operator=(DumbArray&& that) noexcept {
DumbArray(std::move(that)).swap(*this);
return *this;
}
对此:
/// @pre that must be in a valid state
/// @post that will be in an undefined but valid state
DumbArray& operator=(DumbArray&& that) noexcept {
swap(that);
return *this;
}
在DumbArray的情况下,在实践中使用更放松的形式可能是值得的,但要注意微妙的错误。
例如
DumbArray x = { .... };
do_something(std::move(x));
// here: we will get a segfault if we implement the fully destructive
// variant. The optimised variant *may* not crash, it may just do
// something_else with some previously-used data.
// depending on your application, this may be a security risk
something_else(x);
您的代码唯一(小)问题是两者之间的功能重复 move_to_this()
和析构函数,这是一个维护问题,你的班级需要改变。当然,可以通过将该部分提取到一个共同的函数中来解决它 destroy()
。
我对Scott Meyers在他的博客文章中讨论的“问题”的批评:
他试图手动优化编译器可以做得同样好的工作,如果它足够聪明的话。五法则可以减少到四法则
- 仅提供通过值和参数获取其参数的复制赋值运算符
- 没有打算写移动赋值运算符(正是你所做的)。
这自动解决了左侧对象的资源被交换到右侧对象的问题,并且如果右侧对象不是临时对象则不会立即释放。
然后,根据复制和交换习语,在复制赋值运算符的实现内部, swap()
将其作为其参数之一作为一个到期对象。如果编译器可以内联后者的析构函数,那么它肯定会消除额外的指针赋值 - 实际上,为什么要保存将要指向的指针 delete
编辑下一步?
我的结论是,遵循完善的习惯而不是为了微观优化而使实现略微复杂化更为简单,这些微优化在成熟编译器的范围内。