13

这是一个可能的定义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),因此上一句未涵盖。— end note ] [ 注意:如果程序将左值转换为 xvalue,同时将该左值传递给库函数(例如,通过使用参数 move(x) 调用函数),则程序实际上是在要求该函数处理该左值作为临时。如果参数是左值,则该实现可以免费优化别名检查,这可能需要。——尾注]

(感谢Howard Hinnant提供报价

v成为从标准模板库中获取的某种可移动类型的对象并考虑调用std::swap(v, v)。在上面的行中, thata = std::move(b);内部就是这种情况,因此参数不是唯一的引用。这违反了上述要求,因此该行在调用时调用未定义的行为 from 。T::operator=(T&& t)this == &ba = std::move(b)std::swap(v, v)

这里的解释是什么?

4

5 回答 5

7

[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将被赋予一个新值 from tmp。并且tmp具有 的原值a。所以除了烧掉很多cpu周期之外,swap(a, a)是一个空操作。

更新

最新的工作草案 N4618已被修改为在要求中明确说明MoveAssignable

t = rv

(其中rv是一个右值),如果并且不引用同一个对象,则只需是赋值之前t的等效值。无论如何,分配后的状态是未指定的。有一个附加说明需要进一步说明:rvtrvrv

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

于 2012-10-29T20:50:54.910 回答
2

Then the expression a = std::move(b); gets executed, the object is already empty, in a state where only destruction is well defined. That will effectively be a no-op, as the object on the left and right hand sides is already empty. The state of the object after the move is still unknown but destructible. The next statement moves the contents back from tmp and that sets the object back to a known state.

于 2012-10-29T20:39:10.540 回答
2

我同意你的分析,事实上 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 的行为是符合标准的,因为标准说实现可以假设自移动分配永远不会发生,因此断言在有效程序中永远不会失败。

但是,我同意霍华德的观点,即这需要工作(并且可以在没有太多麻烦的情况下工作 - 对于 libstdc++,我们只需要删除调试模式断言!),因此我们需要修复标准以对自移动,或至少自交换。一段时间以来,我一直承诺写一篇关于这个问题的论文,但还没有这样做。

我相信,自从在这里写下他的答案后,霍华德现在同意标准中的当前措辞存在问题,我们需要修复它以禁止 libstdc++ 做出失败的断言。

于 2015-03-16T00:37:10.850 回答
0

我认为这不是一个有效的定义,std::swap因为std::swap被定义为采用左值引用,而不是右值引用(20.2.2 [utility.swap])

于 2012-10-29T20:35:10.373 回答
0

我的理解是直到最近才考虑到这个问题,因此 C++20 标准中的现有措辞并没有真正解决这个问题。

C++23 工作草案 N4885 包括图书馆工作组第 2839 期决议,位于[lib.types.movedfrom]/2

C++ 标准库中定义的类型的对象可以移动分配(11.4.6 [class.copy.assign])给它自己。除非另有说明,否则此类分配会将对象置于有效但未指定的状态。

这使得std::swap标准库类型完全有效。

于 2021-04-05T11:54:34.643 回答