为什么有些人使用交换分配移动?
例如,stdlibc ++具有以下内容:
unique_lock& operator=(unique_lock&& __u) { if(_M_owns) unlock(); unique_lock(std::move(__u)).swap(*this); __u._M_device = 0; __u._M_owns = false; return *this; }
为什么不直接将这两个__u成员分配给* this? 不交换意味着__u被分配了*这个成员,只是以后再分配0和false …在这种情况下交换是做不必要的工作。 我错过了什么? (unique_lock :: swap只是在每个成员上执行std :: swap)
我的错。 (半开玩笑,半开玩笑)。
当我第一次展示移动赋值操作符的示例实现时,我只是使用了swap。 然后,一个聪明的人(我不记得是谁)指出,在分配之前破坏lhs的副作用可能是重要的(例如你的例子中的unlock())。 所以我停止使用交换移动分配。 但是使用交换的历史依然存在,并且一直存在。
在这个例子中没有理由使用交换。 它比你的build议效率低。 的确,在libc ++中 ,我完全按照你的build议:
unique_lock& operator=(unique_lock&& __u) { if (__owns_) __m_->unlock(); __m_ = __u.__m_; __owns_ = __u.__owns_; __u.__m_ = nullptr; __u.__owns_ = false; return *this; }
一般来说,移动赋值操作符应该:
- 销毁可见资源(尽pipe可能保存实现细节资源)。
- 移动分配所有的基地和成员。
- 如果基地和成员的移动任务没有使得资源less,那就这样做。
像这样:
unique_lock& operator=(unique_lock&& __u) { // 1. Destroy visible resources if (__owns_) __m_->unlock(); // 2. Move assign all bases and members. __m_ = __u.__m_; __owns_ = __u.__owns_; // 3. If the move assignment of bases and members didn't, // make the rhs resource-less, then make it so. __u.__m_ = nullptr; __u.__owns_ = false; return *this; }
更新
在注释中有关于如何处理移动构造函数的后续问题。 我开始在那里(在评论中)回答,但是格式和长度限制使得难以创build明确的响应。 因此,我正在把我的回应放在这里。
问题是:创build移动构造函数的最佳模式是什么? 委托给默认的构造函数,然后交换? 这具有减less代码重复的优点。
我的回答是:我认为最重要的一点就是程序员应该不加思索地遵循模式。 可能有一些类实现移动构造函数作为默认+交换是完全正确的答案。 class级可能很大,很复杂。 A(A&&) = default;
可能会做错事。 我认为考虑每个class级的所有select是很重要的。
让我们来看看OP的例子: std::unique_lock(unique_lock&&)
。
观察:
答:这个class很简单。 它有两个数据成员:
mutex_type* __m_; bool __owns_;
B.这个类是在一个通用的库中,供不知名的客户使用。 在这种情况下,性能问题是重中之重。 我们不知道我们的客户是否会在性能严重的代码中使用这个类。 所以我们必须假设他们是。
C.这个类的移动构造函数将包含less量的加载和存储,无论如何。 因此,查看性能的一个好方法是统计加载和存储。 例如,如果您使用4个商店做某件事,而其他人只用2个商店做同样的事情,那么您的两个实施都非常快。 但他们的速度是你的速度的两倍 ! 这种差异在一些客户的紧张环境中可能是至关重要的。
首先让我们计算加载和存储的默认构造函数,并在成员交换函数:
// 2 stores unique_lock() : __m_(nullptr), __owns_(false) { } // 4 stores, 4 loads void swap(unique_lock& __u) { std::swap(__m_, __u.__m_); std::swap(__owns_, __u.__owns_); }
现在让我们以两种方式实现移动构造器:
// 4 stores, 2 loads unique_lock(unique_lock&& __u) : __m_(__u.__m_), __owns_(__u.__owns_) { __u.__m_ = nullptr; __u.__owns_ = false; } // 6 stores, 4 loads unique_lock(unique_lock&& __u) : unique_lock() { swap(__u); }
第一种方式比第二种方式看起来复杂得多。 而且源代码更大,并且有些重复的代码可能已经写在其他地方(比如在移动赋值操作符中)。 这意味着有更多的错误机会。
第二种方法更简单,重用已经写好的代码。 从而减less错误的机会。
第一种方法更快。 如果加载和存储的成本大致相同,则可能快66%!
这是一个典型的工程交易。 天下没有免费的午餐。 而且工程师们也从来没有放弃必须作出权衡决定的负担。 一分钟,飞机开始从空中坠落,核电站开始融化。
对于libc ++ ,我select了更快的解决scheme。 我的理由是,对于这个class级,无论做什么,我都会做得更好。 这个class很简单,我的机会很高。 而我的客户将会重视性能。 我可能在不同的语境下得出另一个不同的阶级的结论。
这是关于exception安全。 由于运算符被调用时__u
已经被构造,所以我们知道没有例外, swap
也不会抛出。
如果你手动完成了成员的任务,你可能会冒险,每个人可能会抛出一个exception,然后你必须处理部分移动分配的东西,但不得不救助。
也许在这个微不足道的例子中,这并不表示,但这是一个通用的devise原则:
- 通过复制构造和交换进行复制分配。
- 通过move-construct和swap进行移动分配。
- 用构造和
+=
等来写+
基本上,你尽量减less“真实”代码的数量,尽可能多地expression核心特性。
( unique_ptr
在赋值中需要显式的右值引用,因为它不允许拷贝构造/赋值,所以它不是这个devise原理的最好例子。)
另一个需要考虑的问题是权衡:
default-construct + swap的实现可能会比较慢,但是在某些情况下,编译器中的数据stream分析可以消除一些无意义的分配,并最终与手写代码非常相似。 这只适用于没有“聪明”价值语义的types。 举个例子,
struct Dummy { Dummy(): x(0), y(0) {} // suppose we require default 0 on these Dummy(Dummy&& other): x(0), y(0) { swap(other); } void swap(Dummy& other) { std::swap(x, other.x); std::swap(y, other.y); text.swap(other.text); } int x, y; std::string text; }
生成的代码在移动ctor没有优化:
<inline std::string() default ctor> x = 0; y = 0; temp = x; x = other.x; other.x = temp; temp = y; y = other.y; other.y = temp; <inline impl of text.swap(other.text)>
这看起来很糟糕,但数据stream分析可以确定它是相当于代码:
x = other.x; other.x = 0; y = other.y; other.y = 0; <overwrite this->text with other.text, set other.text to default>
也许在实践中编译器不会总是产生最佳版本。 可能想试验一下,并看一看assembly。
还有一种情况是,由于“巧妙”的值语义,交换比赋值更好,例如,如果类中的一个成员是std :: shared_ptr。 没有理由移动构造函数应该搞乱primefacesrefcounter。
我会从标题中回答这个问题:“为什么有些人使用交换分配?”。
使用swap
主要原因是提供noexcept移动分配 。
来自Howard Hinnant的评论:
一般来说,移动赋值操作符应该:
1.销毁可见的资源(尽pipe也许保存实现细节资源)。
但是一般来说destroy / release函数可能会失败并抛出exception !
这里是一个例子:
class unix_fd { int fd; public: explicit unix_fd(int f = -1) : fd(f) {} ~unix_fd() { if(fd == -1) return; if(::close(fd)) /* !!! call is failed! But we can't throw from destructor so just silently ignore....*/; } void close() // Our release-function { if(::close(fd)) throw system_error_with_errno_code; } };
现在我们来比较两种移动赋值的实现:
// #1 void unix_fd::operator=(unix_fd &&o) // Can't be noexcept { if(&o != this) { close(); // !!! Can throw here fd = o.fd; o.fd = -1; } return *this; }
和
// #2 void unix_fd::operator=(unix_fd &&o) noexcept { std::swap(fd, o.fd); return *this; }
#2
是完全noexcept!
是的,在情况#2
, close()
调用可以被“延迟”。 但! 如果我们想要严格的错误检查,我们必须使用显式的close()
调用,而不是析构函数。 析构函数仅在“紧急”情况下释放资源,无论如何都不能抛出exception。
PS另请参见讨论在这里的意见