繁体   English   中英

关于根据移动赋值和移动构造函数实现 std::swap

[英]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);
}

我相信

  1. std::swap(v,v)保证没有效果并且
  2. 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是一个右值),如果trv不引用同一个对象,则t只需要是赋值前rv的等效值。 无论如何, rv的状态在赋值后是未指定的。 还有一个附加说明需要进一步说明:

rv必须仍然满足使用它的库组件的要求,无论trv是否引用同一个对象。

然后表达式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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM