在移动赋值和移动构造函数方面实现std :: swap

时间:2012-10-29 20:21:28

标签: c++ stl c++11 move-semantics

以下是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可按上述方式实施。
  3. 以下引言似乎暗示这些信念是矛盾的。

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

      

    1以下各项适用于定义的函数的所有参数   除非另有明确说明,否则在C ++标准库中。

         

    ...

         
        
    • 如果函数参数绑定到右值引用参数,则实现可以假定此参数是对其的唯一引用   这个论点。 [注意:如果参数是通用参数   形式T&amp;&amp;并且绑定了一个类型A的左值,该参数绑定到一个   左值参考(14.8.2.1)因此不在前面   句子。 - 结束注释] [注意:如果程序将左值转换为   将左值传递给库函数时的xvalue(例如通过   用参数move(x)调用函数,程序是   有效地要求该函数将该左值作为临时值处理。   实现可以自由地优化别名检查   如果参数是左值,则可能需要。 -endnote]
    •   

    (感谢Howard Hinnantproviding the quote

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

    这里有什么解释?

5 个答案:

答案 0 :(得分:6)

[res.on.arguments]是关于客户端应如何使用std :: lib的声明。当客户端向std :: lib函数发送xvalue时,客户端必须愿意假装xvalue确实是prvalue,并期望std :: lib利用它。

但是当客户端调用std :: swap(x,x)时,客户端没有向std :: lib函数发送xvalue。这是实现,而是这样做。因此,实现的责任在于使std :: swap(x,x)工作。

话虽如此,std已给予实现者一个保证:X应满足MoveAssignable。即使处于移动状态,客户端也必须确保X是MoveAssignable。此外,std::swap的实现并不真正关心自移动赋值的作用,只要它不是X的未定义行为即可。只要它没有崩溃。

a = std::move(b);

当&amp; a ==&amp; b时,此分配的源和目标都具有未指定(移动)值。这可以是无操作,也可以做其他事情。只要它不崩溃,std :: swap就能正常工作。这是因为在下一行:

b = std::move(tmp);

从前一行a进入的任何值都将从tmp获得一个新值。 tmp的原始值为a。因此,除了烧掉大量的cpu周期外,swap(a, a)还是一个无操作。

<强>更新

latest working draft, N4618已被修改,以明确说明MoveAssignable要求中的表达式:

t = rv

(其中rv是右值),如果trv没有t,则rv只需要在分配之前为rv的等效值引用相同的对象。无论如何,rv的状态在分配后未指定。还有一个说明需要进一步澄清:

  无论trv是否引用相同的对象,

{{1}}仍必须满足使用它的库组件的要求。

答案 1 :(得分:3)

然后表达式a = std::move(b);被执行,对象已经为空,处于一个只有很好定义破坏的状态。这实际上是无操作,因为左侧和右侧的物体已经是空的。移动后物体的状态仍然是未知但可破坏的。下一个语句将内容从tmp移回,并将对象设置回已知状态。

答案 2 :(得分: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)

(我不知道&#34; PST&#34;应该是什么意思!我认为我测试的安装有问题。)

我相信GCC在这里的行为是符合的,因为标准说实现可以假设自动分配永远不会发生,因此断言永远不会在有效的程序中失败。

但是,我同意Howard的说法,这需要工作(并且可以毫无困难地工作 - 对于libstdc ++,我们只需要删除调试模式断言!),所以我们需要修改标准来制作自我移动或至少自我交换的例外。我一直承诺在一段时间内写一篇关于这个问题的论文,但还没有这样做。

我相信,既然在这里写答案,霍华德现在同意标准中目前的措辞存在问题,我们需要修复它以禁止libstdc ++使该断言失败。

答案 3 :(得分:0)

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

答案 4 :(得分:0)

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

C++23 工作草案 N4885 在 [lib.types.movedfrom]/2 处包含 Library Working Group issue 2839 resolution

<块引用>

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

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