[英]On implementing std::swap in terms of move assignment and move constructor
这是std::swap
的可能定义:
template<class T>
void swap(T& a, T& b) {
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}
我相信
std::swap(v,v)
保证没有效果并且std::swap
可以如上实现。以下引述在我看来暗示这些信念是矛盾的。
17.6.4.9 函数参数 [res.on.arguments]
1 以下各项适用于 C++ 标准库中定义的函数的所有参数,除非另有明确说明。
...
- 如果函数参数绑定到右值引用参数,实现可能会假定此参数是对该参数的唯一引用。 [ 注意:如果参数是 T&& 形式的泛型参数并且绑定了类型 A 的左值,则参数绑定到左值引用 (14.8.2.1),因此不在上一句中。 — 尾注] [ 注意:如果程序在将左值传递给库函数时将左值强制转换为 xvalue(例如,通过使用参数 move(x) 调用函数),程序实际上是在要求该函数处理该左值作为临时。 该实现可以自由优化别名检查,如果参数是左值,则可能需要进行别名检查。 ——尾注]
(感谢Howard Hinnant 提供报价)
设v
是从标准模板库中获取的某种可移动类型的对象,并考虑调用std::swap(v, v)
。 在行a = std::move(b);
在上面, T::operator=(T&& t)
内部的情况是this == &b
,因此参数不是唯一引用。 这违反了上述要求,因此当从std::swap(v, v)
调用时,行a = std::move(b)
调用未定义的行为。
这里的解释是什么?
[res.on.arguments] 是关于客户端应如何使用 std::lib 的声明。 当客户端向 std::lib 函数发送一个 xvalue 时,客户端必须愿意假装 xvalue 实际上是一个纯右值,并期望 std::lib 能够利用它。
然而,当客户端调用 std::swap(x, x) 时,客户端不会将 xvalue 发送到 std::lib 函数。 相反,是实现这样做了。 因此,使 std::swap(x, x) 工作的责任在于实现。
话虽这么说,std 已经给了实现者一个保证:X 应该满足MoveAssignable
。 即使处于移出状态,客户端也必须确保 X 是 MoveAssignable。 此外, std::swap
的实现并不真正关心自我移动赋值的作用,只要它不是 XIe 的未定义行为,只要它不崩溃即可。
a = std::move(b);
当 &a == &b 时,此赋值的源和目标都有一个未指定(移出)的值。 这可以是空操作,也可以做其他事情。 只要它不崩溃,std::swap 就会正常工作。 这是因为在下一行中:
b = std::move(tmp);
从上一行进入a
的任何值都将从tmp
中获得一个新值。 tmp
的原始值为a
。 所以除了消耗大量的 cpu 周期之外, swap(a, a)
是一个空操作。
更新
最新的工作草案 N4618已被修改为明确指出在MoveAssignable
要求表达式中:
t = rv
(其中rv
是一个右值),如果t
和rv
不引用同一个对象,则t
只需要是赋值前rv
的等效值。 无论如何, rv
的状态在赋值后是未指定的。 还有一个附加说明需要进一步说明:
rv
必须仍然满足使用它的库组件的要求,无论t
和rv
是否引用同一个对象。
然后表达式a = std::move(b);
被执行时,对象已经是空的,处于只有销毁定义明确的状态。 这实际上是一个空操作,因为左侧和右侧的对象已经是空的。 移动后对象的状态仍然未知但可以破坏。 下一条语句将内容从tmp
移回并将对象设置回已知状态。
我同意你的分析,事实上 libstdc++ 调试模式有一个断言会在标准容器的自交换时触发:
#include <vector>
#include <utility>
struct S {
std::vector<int> v;
};
int main()
{
S s;
std::swap(s, s);
}
包装器类型S
是必需的,因为交换向量直接使用调用vector::swap()
的特化,因此不使用通用的std::swap
,但S
将使用通用的,并且在编译为 C++11 时这将导致矢量成员的自我移动分配,这将中止:
/home/toor/gcc/4.8.2/include/c++/4.8.2/debug/vector:159:error: PST.
Objects involved in the operation:
sequence "this" @ 0x0x7fffe8fecc00 {
type = NSt7__debug6vectorIiSaIiEEE;
}
Aborted (core dumped)
(我不知道“PST”在那里应该是什么意思。我认为我测试过的安装有问题。)
我相信 GCC 在这里的行为是符合标准的,因为标准说实现可以假设自移动赋值永远不会发生,因此断言在有效程序中永远不会失败。
然而,我同意 Howard 的观点,这需要工作(并且可以在没有太多麻烦的情况下工作——对于 libstdc++,我们只需要删除调试模式断言,),所以我们需要修复标准,为自我移动。 或者至少自我交换,一段时间以来我一直承诺要写一篇关于这个问题的论文。 但还没有这样做。
我相信,既然在这里写了他的回答,霍华德现在同意标准中当前的措辞存在问题,我们需要修复它以禁止 libstdc++ 做出失败的断言。
我认为这不是std::swap
的有效定义,因为std::swap
被定义为采用左值引用,而不是右值引用(20.2.2 [utility.swap])
我的理解是这个问题直到最近才被考虑,所以 C++20 标准中的现有措辞并没有真正解决它。
C++23 工作草案 N4885 在[lib.types.movedfrom]/2
包含库工作组问题 2839 决议:
C++ 标准库中定义的类型的对象可以移动分配 (11.4.6 [class.copy.assign]) 到自身。 除非另有说明,否则此类分配会将对象置于有效但未指定的状态。
这使得std::swap
对标准库类型完全有效。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.