这是我的错(半开玩笑,半认真)。
当我第一次展示移动赋值运算符的实现示例时,我只使用了swap。然后有个聪明的家伙(我记不清是谁了)指出,在赋值之前销毁lhs的副作用可能很重要(例如您示例中的unlock())。所以我停止使用swap进行移动赋值。但是使用swap的历史仍然存在并且仍在继续。
在这个例子中没有理由使用swap。它比您建议的方法效率低。实际上,在libc ++中,我正是按照您的建议做的:
unique_lock& operator=(unique_lock&& __u)
{
if (__owns_)
__m_->unlock();
__m_ = __u.__m_;
__owns_ = __u.__owns_;
__u.__m_ = nullptr;
__u.__owns_ = false;
return *this;
}
一般而言,移动赋值运算符应该:
- 销毁可见资源(但可以保存实现细节资源)。
- 移动赋值所有基类和成员。
- 如果基类和成员的移动赋值没有使rhs变为无资源状态,则使其变为无资源状态。
如下所示:
unique_lock& operator=(unique_lock&& __u)
{
if (__owns_)
__m_->unlock();
__m_ = __u.__m_;
__owns_ = __u.__owns_;
__u.__m_ = nullptr;
__u.__owns_ = false;
return *this;
}
更新
在评论中有一个关于如何处理移动构造函数的跟进问题。我开始在那里回答(在评论中),但格式和长度限制使得很难创建清晰的响应。因此,我将我的回应放在这里。
问题是:创建移动构造函数的最佳模式是什么?委托给默认构造函数然后交换?这样做的好处是减少代码重复。
我的回答是:我认为最重要的经验教训是程序员应该谨慎地遵循模式而不加思考。也许有些类实现移动构造函数作为默认+交换恰恰是正确的答案。这个类可能又大又复杂。 A(A&&) = default;
可能会出错。我认为考虑每个类的所有选择非常重要。
让我们详细看一下 OP 的例子:std::unique_lock(unique_lock&&)
。
观察:
A. 这个类相当简单。它有两个数据成员:
mutex_type* __m_;
bool __owns_;
B. 这个类在通用库中,将被未知数量的客户使用。在这种情况下,性能问题是一个高优先级。我们不知道我们的客户是否会在性能关键代码中使用这个类。因此,我们必须假设他们会。
C. 无论如何,这个类的移动构造函数都将由少量的加载和存储组成。因此,评估性能的好方法是计算加载和存储次数。例如,如果您使用4个存储器执行某些操作,而其他人只使用2个存储器执行相同的操作,则两个实现都非常快。但是他们的速度是你的两倍!这种差异可能在某些客户的紧密循环中非常重要。
首先,让我们计算默认构造函数和成员交换函数中的加载和存储次数:
unique_lock()
: __m_(nullptr),
__owns_(false)
{
}
void swap(unique_lock& __u)
{
std::swap(__m_, __u.__m_);
std::swap(__owns_, __u.__owns_);
}
现在让我们以两种方式实现移动构造函数:
unique_lock(unique_lock&& __u)
: __m_(__u.__m_),
__owns_(__u.__owns_)
{
__u.__m_ = nullptr;
__u.__owns_ = false;
}
unique_lock(unique_lock&& __u)
: unique_lock()
{
swap(__u);
}
第一种方法看起来比第二种复杂得多。源代码更大,有些代码可能已经在其他地方编写过了(比如移动赋值运算符)。这意味着出现错误的机会更多。
第二种方法更简单,并且重用了我们已经编写的代码。因此出错的可能性较小。
第一种方法更快。如果加载和存储的成本大致相同,那么速度可能快66%!
这是一个经典的工程权衡。没有免费的午餐。工程师们永远不会摆脱必须做出权衡决策的负担。一旦做出决定,飞机就开始从空中掉落,核电站就开始熔毁。
对于
libc ++,我选择了更快的解决方案。我的理由是,对于这个类,无论如何都要做到正确;这个类足够简单,我做对的机会很高;而我的客户将重视性能。在不同上下文环境中的不同类别可能会得出不同的结论。
T::operator=(T&&)
而不是使用swap
。感谢您的好解释。但是Move
T::T(T&&)
怎么样? - towi