复制和交换惯用法是否应该成为 C++11 中的复制和移动惯用法?
-
21-12-2019 - |
题
正如中所解释的 这个答案, ,复制和交换习惯用法的实现如下:
class MyClass
{
private:
BigClass data;
UnmovableClass *dataPtr;
public:
MyClass()
: data(), dataPtr(new UnmovableClass) { }
MyClass(const MyClass& other)
: data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
MyClass(MyClass&& other)
: data(std::move(other.data)), dataPtr(other.dataPtr)
{ other.dataPtr= nullptr; }
~MyClass() { delete dataPtr; }
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, other.data);
swap(first.dataPtr, other.dataPtr);
}
MyClass& operator=(MyClass other)
{
swap(*this, other);
return *this;
}
};
通过将 MyClass 值作为 operator= 的参数,可以通过复制构造函数或移动构造函数构造该参数。然后,您可以安全地从参数中提取数据。这可以防止代码重复并有助于异常安全。
答案提到您可以交换或移动临时变量。它主要讨论交换。但是,如果编译器未优化交换,则交换涉及三个移动操作,并且在更复杂的情况下会执行额外的额外工作。当你想要的只是 移动 临时到分配给对象中。
考虑这个更复杂的例子,涉及 观察者模式. 。在此示例中,我手动编写了赋值运算符代码。重点是移动构造函数、赋值运算符和交换方法:
class MyClass : Observable::IObserver
{
private:
std::shared_ptr<Observable> observable;
public:
MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
friend void swap(MyClass& first, MyClass& second)
{
//Checks for nullptr and same observable omitted
using std::swap;
swap(first.observable, second.observable);
second.observable->unregisterObserver(first);
first.observable->registerObserver(first);
first.observable->unregisterObserver(second);
second.observable->registerObserver(second);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
}
显然,这个手动编写的赋值运算符中的代码重复部分与移动构造函数的代码相同。您可以在赋值运算符中执行交换,并且行为是正确的,但它可能会执行更多移动并执行额外的注册(在交换中)和取消注册(在析构函数中)。
重用移动构造函数的代码不是更有意义吗?
private:
void performMoveActions(MyClass&& other)
{
observable->unregisterObserver(other);
other.observable.reset(nullptr);
observable->registerObserver(*this);
}
public:
MyClass(MyClass&& other) : observable(std::move(other.observable))
{
performMoveActions(other);
}
MyClass& operator=(MyClass other)
{
observable->unregisterObserver(*this);
observable = std::move(other.observable);
performMoveActions(other);
}
在我看来,这种方法永远不逊色于交换方法。我是否正确地认为复制和交换惯用法会比 C++11 中的复制和移动惯用法更好,还是我错过了一些重要的东西?
解决方案 3
自从我问这个问题以来已经很久了,我现在已经知道了一段时间的答案,但我已经推迟了答案的答案。这是。
答案是否定的。复制和交换成语不应成为复制和移动的习语。
复制和交换的重要组成部分(也是移动构造和交换)是实现具有安全清理的分配运算符的方法。旧数据被交换为复制构造或移动构造的临时。完成操作后,临时被删除,并调用其析构函数。
交换行为是能够重用析构函数,因此您不必在赋值运营商中编写任何清理代码。
如果没有要完成的清理行为,并且只有分配,那么您应该能够将分配运算符声明为默认值并不需要复制和交换。
移动构造函数本身通常不需要任何清理行为,因为它是一个新对象。一般简单的方法是使Move构造函数调用默认构造函数,然后将所有成员与移动到Object交换。然后移动到Object将像Bland默认构造的对象一样。
但是,在此问题的观察者模式示例中,它实际上是一个例外,您必须执行额外的清理工作,因为需要更改对旧对象的引用。一般来说,我建议您的观察者和观察到,以及基于参考的其他设计构造,只要有可能。其他提示
给予每一位特殊成员应有的温柔关爱,并尽量默认他们:
class MyClass
{
private:
BigClass data;
std::unique_ptr<UnmovableClass> dataPtr;
public:
MyClass() = default;
~MyClass() = default;
MyClass(const MyClass& other)
: data(other.data)
, dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr)
{ }
MyClass& operator=(const MyClass& other)
{
if (this != &other)
{
data = other.data;
dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
: nullptr);
}
return *this;
}
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;
friend void swap(MyClass& first, MyClass& second)
{
using std::swap;
swap(first.data, second.data);
swap(first.dataPtr, second.dataPtr);
}
};
如果需要,析构函数可以在上面隐式默认。对于此示例,其他所有内容都需要显式定义或默认。
参考: http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf
复制/交换习惯用法可能会降低性能(请参阅幻灯片)。例如,你想知道为什么高性能/经常使用 std::types 像 std::vector
和 std::string
不使用复制/交换?业绩不佳就是原因。如果 BigClass
包含任何 std::vector
或 std::string
(这似乎是可能的),你最好的选择是从你的特殊成员中调用他们的特殊成员。以上就是如何做到这一点。
如果您在作业中需要强大的异常安全性,请参阅幻灯片了解如何在性能之外提供此功能(搜索“strong_assign”)。
swap
函数。默认的世代swap
将诉诸MOVE:
void swap(T& left, T& right) {
T tmp(std::move(left));
left = std::move(right);
right = std::move(tmp);
}
.
就是这样,元素被交换。
第二,基于此,复制和交换实际上仍然存在:
T& T::operator=(T const& left) {
using std::swap;
T tmp(left);
swap(*this, tmp);
return *this;
}
// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;
.
编辑:这只实现复制分配运算符;还需要一个单独的移动分配运算符,但它可以默认,否则将发生堆栈溢出(移动分配和互相拨打互相调用)。