简体   繁体   English

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

[英]On implementing std::swap in terms of move assignment and move constructor

Here is a possible definition of std::swap :这是std::swap的可能定义:

template<class T>
void swap(T& a, T& b) {
  T tmp(std::move(a));
  a = std::move(b);
  b = std::move(tmp);
}

I believe that我相信

  1. std::swap(v,v) is guaranteed to have no effects and std::swap(v,v)保证没有效果并且
  2. std::swap can be implemented as above. std::swap可以如上实现。

The following quote seems to me to imply that these beliefs are contradictory.以下引述在我看来暗示这些信念是矛盾的。

17.6.4.9 Function arguments [res.on.arguments] 17.6.4.9 函数参数 [res.on.arguments]

1 Each of the following applies to all arguments to functions defined in the C++ standard library, unless explicitly stated otherwise. 1 以下各项适用于 C++ 标准库中定义的函数的所有参数,除非另有明确说明。

... ...

  • If a function argument binds to an rvalue reference parameter, the implementation may assume that this parameter is a unique reference to this argument.如果函数参数绑定到右值引用参数,实现可能会假定此参数是对该参数的唯一引用。 [ Note: If the parameter is a generic parameter of the form T&& and an lvalue of type A is bound, the argument binds to an lvalue reference (14.8.2.1) and thus is not covered by the previous sentence. [ 注意:如果参数是 T&& 形式的泛型参数并且绑定了类型 A 的左值,则参数绑定到左值引用 (14.8.2.1),因此不在上一句中。 — end note ] [ Note: If a program casts an lvalue to an xvalue while passing that lvalue to a library function (eg by calling the function with the argument move(x)), the program is effectively asking that function to treat that lvalue as a temporary. — 尾注] [ 注意:如果程序在将左值传递给库函数时将左值强制转换为 xvalue(例如,通过使用参数 move(x) 调用函数),程序实际上是在要求该函数处理该左值作为临时。 The implementation is free to optimize away aliasing checks which might be needed if the argument was an lvalue.该实现可以自由优化别名检查,如果参数是左值,则可能需要进行别名检查。 —endnote] ——尾注]

(thanks to Howard Hinnant for providing the quote ) (感谢Howard Hinnant 提供报价

Let v be an object of some movable type taken from the Standard Template Library and consider the call std::swap(v, v) .v是从标准模板库中获取的某种可移动类型的对象,并考虑调用std::swap(v, v) In the line a = std::move(b);在行a = std::move(b); above, it is the case inside T::operator=(T&& t) that this == &b , so the parameter is not a unique reference.在上面, T::operator=(T&& t)内部的情况是this == &b ,因此参数不是唯一引用。 That is a violation of the requirement made above, so the line a = std::move(b) invokes undefined behavior when called from std::swap(v, v) .这违反了上述要求,因此当从std::swap(v, v)调用时,行a = std::move(b)调用未定义的行为。

What is the explanation here?这里的解释是什么?

[res.on.arguments] is a statement about how the client should use the std::lib. [res.on.arguments] 是关于客户端应如何使用 std::lib 的声明。 When the client sends an xvalue to a std::lib function, the client has to be willing to pretend that the xvalue is really a prvalue, and expect the std::lib to take advantage of that.当客户端向 std::lib 函数发送一个 xvalue 时,客户端必须愿意假装 xvalue 实际上是一个纯右值,并期望 std::lib 能够利用它。

However when the client calls std::swap(x, x), the client isn't sending an xvalue to a std::lib function.然而,当客户端调用 std::swap(x, x) 时,客户端不会将 xvalue 发送到 std::lib 函数。 It is the implementation that is doing so instead.相反,是实现这样做了。 And so the onus is on the implementation to make std::swap(x, x) work.因此,使 std::swap(x, x) 工作的责任在于实现。

That being said, the std has given the implementor a guarantee: X shall satisfy MoveAssignable .话虽这么说,std 已经给了实现者一个保证:X 应该满足MoveAssignable Even if in a moved-from state, the client must ensure that X is MoveAssignable.即使处于移出状态,客户端也必须确保 X 是 MoveAssignable。 Furthermore, the implementation of std::swap doesn't really care what self-move-assignment does, as long as it is not undefined behavior for XIe as long as it doesn't crash.此外, std::swap的实现并不真正关心自我移动赋值的作用,只要它不是 XIe 的未定义行为,只要它不崩溃即可。

a = std::move(b);

When &a == &b, both the source and target of this assignment have an unspecified (moved-from) value.当 &a == &b 时,此赋值的源和目标都有一个未指定(移出)的值。 This can be a no-op, or it can do something else.这可以是空操作,也可以做其他事情。 As long as it doesn't crash, std::swap will work correctly.只要它不崩溃,std::swap 就会正常工作。 This is because in the next line:这是因为在下一行中:

b = std::move(tmp);

Whatever value went into a from the previous line is going to be given a new value from tmp .从上一行进入a的任何值都将从tmp中获得一个新值。 And tmp has the original value of a . tmp的原始值为a So besides burning up a lot of cpu cycles, swap(a, a) is a no-op.所以除了消耗大量的 cpu 周期之外, swap(a, a)是一个空操作。

Update更新

The latest working draft, N4618 has been modified to clearly state that in the MoveAssignable requirements the expression: 最新的工作草案 N4618已被修改为明确指出在MoveAssignable要求表达式中:

t = rv

(where rv is an rvalue), t need only be the equivalent value of rv prior to the assignment if t and rv do not reference the same object. (其中rv是一个右值),如果trv不引用同一个对象,则t只需要是赋值前rv的等效值。 And regardless, rv 's state is unspecified after the assignment.无论如何, rv的状态在赋值后是未指定的。 There is an additional note for further clarification:还有一个附加说明需要进一步说明:

rv must still meet the requirements of the library component that is using it, whether or not t and rv refer to the same object. rv必须仍然满足使用它的库组件的要求,无论trv是否引用同一个对象。

Then the expression a = std::move(b);然后表达式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.下一条语句将内容从tmp移回并将对象设置回已知状态。

I agree with your analysis, and in fact the libstdc++ Debug Mode has an assertion that will fire on self-swap of standard containers:我同意你的分析,事实上 libstdc++ 调试模式有一个断言会在标准容器的自交换时触发:

#include <vector>
#include <utility>

struct S {
  std::vector<int> v;
};

int main()
{
  S s;
  std::swap(s, s);
}

The wrapper type S is needed because swapping vector directly uses the specialization that calls vector::swap() and so doesn't use the generic std::swap , but S will use the generic one, and when compiled as C++11 that will result in a self-move-assignment of the vector member, which will abort:包装器类型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)

(I don't know what "PST" is supposed to mean there. I think something is wrong with the installation I tested it with.) (我不知道“PST”在那里应该是什么意思。我认为我测试过的安装有问题。)

I believe GCC's behaviour here is conforming, because the standard says that the implementation can assume that self-move-assignment never happens, therefore the assertion will never fail in a valid program.我相信 GCC 在这里的行为是符合标准的,因为标准说实现可以假设自移动赋值永远不会发生,因此断言在有效程序中永远不会失败。

However, I agree with Howard that this needs to work (and can be made to work without too much trouble - for libstdc++ we just need to delete the debug mode assertion,), and so we need to fix the standard to make an exception for self-move.然而,我同意 Howard 的观点,这需要工作(并且可以在没有太多麻烦的情况下工作——对于 libstdc++,我们只需要删除调试模式断言,),所以我们需要修复标准,为自我移动。 or at least self-swap, I have been promising to write a paper about this issue for some time.或者至少自我交换,一段时间以来我一直承诺要写一篇关于这个问题的论文。 but haven't done so yet.但还没有这样做。

I believe that since writing his answer here Howard now agrees there is a problem with the current wording in the standard, and we need to fix it to forbid libstdc++ from making that assertion that fails.我相信,既然在这里写了他的回答,霍华德现在同意标准中当前的措辞存在问题,我们需要修复它以禁止 libstdc++ 做出失败的断言。

I believe that is not a valid definition of std::swap because std::swap is defined to take lvalue references, not rvalue references (20.2.2 [utility.swap])我认为这不是std::swap的有效定义,因为std::swap被定义为采用左值引用,而不是右值引用(20.2.2 [utility.swap])

My understanding is that the issue was not thought about until recently, so existing wording in C++20 standard does not really address it.我的理解是这个问题直到最近才被考虑,所以 C++20 标准中的现有措辞并没有真正解决它。

C++23 working draft N4885 includes Library Working Group issue 2839 resolution at [lib.types.movedfrom]/2 : C++23 工作草案 N4885 在[lib.types.movedfrom]/2包含库工作组问题 2839 决议

An object of a type defined in the C++ standard library may be move-assigned (11.4.6 [class.copy.assign]) to itself. C++ 标准库中定义的类型的对象可以移动分配 (11.4.6 [class.copy.assign]) 到自身。 Such an assignment places the object in a valid but unspecified state unless otherwise specified.除非另有说明,否则此类分配会将对象置于有效但未指定的状态。

That makes such std::swap perfectly valid for standard library types.这使得std::swap对标准库类型完全有效。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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