避免在std :: shuffle中进行自我赋值

时间:2014-04-07 14:28:20

标签: c++11 g++

在使用glibcxx的检查实现时,我偶然发现了以下问题:

/usr/include/c++/4.8.2/debug/vector:159:error: attempt to self move assign.
Objects involved in the operation:
sequence "this" @ 0x0x1b3f088 {
  type = NSt7__debug6vectorIiSaIiEEE;
}

我已经简化为这个最小的例子:

#include <vector>
#include <random>
#include <algorithm>

struct Type {
        std::vector<int> ints;
};

int main() {
        std::vector<Type> intVectors = {shuffle, {{1, 2}}};
        std::shuffle(intVectors.begin(), intVectors.end(), std::mt19937());
}

追踪问题我发现std::swap想要Type一个元素。由于std::swap是用户定义的,并且没有为operator=(&&)提供专门化,因此使用默认值创建临时值并使用 _Tp __tmp = _GLIBCXX_MOVE(__a); __a = _GLIBCXX_MOVE(__b); __b = _GLIBCXX_MOVE(__tmp); 来传输值:

Type

由于operator=(&&)未明确提供__a,因此默认情况下由&#34;递归地执行&#34;对其成员应用相同的操作。

问题出现在交换代码的第2行,其中__b__a.operator=(std::move(__a))指向同一个对象,该对象在代码vector::operator=(&&)中生效,然后在检查中触发错误实施swap

我的问题是:这是谁的错?

  • 是我的,因为我应该为NOP提供一个实现自我交换&#34;一个std::shuffle
  • #include <vector> int main() { std::vector<int> vectorOfInts; vectorOfInts = std::move(vectorOfInts); } &#39; s,因为它不应该尝试与自己交换元素?
  • 这是经过检查的实施,因为自动转移是完全正常的吗?
  • 一切都是正确的,经过检查的实施只是帮我做了额外的检查(但是如何将其关闭)?

我读过有关要求迭代器为ValueSwappable的shuffle。这是否扩展到自交换(这只是运行时问题,无法通过编译时概念检查强制执行)?

附录

要更直接地触发错误,可以使用:

std::vector

当然这很明显(你为什么要把一个矢量移到自己身上?)。 如果您直接交换operator=(&&),则不会发生错误,因为矢量类具有不使用{{1}}的交换函数的自定义实现。

3 个答案:

答案 0 :(得分:5)

这是GCC检查实施中的一个错误。根据{{​​3}},可交换的要求包括(强调我的):

  

17.6.3.2§4当且仅当t可以分别与任何 rvalue或lvalue类型{{1}进行交换时,rvalue或左值t是可交换的}}

根据定义,任何 rvalue或左值都包含T本身,因此可以交换t必须合法。同时,默认swap(t,t)实现需要以下

  

20.2.2§2要求:类型swap应为MoveConstructible(表20)和MoveAssignable(表22)。

因此,要在默认交换运算符的定义下可交换,自动赋值必须是有效的并且具有自我赋值T之后的后置条件等同于它的旧值(不一定是无操作值) !)如表22所示。

虽然您交换的对象不是标准类型,但MoveAssignable没有前提条件trv引用不同的对象,并且只要所有成员都是MoveAssignable({{1}应该是)生成移动赋值运算符必须是正确的(因为它按照12.8§29执行成员移动赋值)。此外,尽管该注释指出t具有有效但未指定的状态,但除了等同于其原始值之外的任何状态对于自我赋值都是不正确的,否则后置条件将被违反。

答案 1 :(得分:5)

libstdc ++ Debug Mode断言基于此标准,来自[res.on.arguments]

  

如果函数参数绑定到右值引用参数,则实现可能会假定此参数是对此参数的唯一引用。

即。实现可以假设绑定到T::operator=(T&&)参数的对象不是别名*this,如果程序违反了该假设,则行为未定义。因此,如果调试模式检测到实际上右值引用 绑定到*this,则它已检测到未定义的行为,因此可以中止。

该段落也包含这一说明(强调我的):

  

[注意:如果程序在将左值传递给库函数时将左值转换为x值(例如,通过使用参数调用函数)   std::move(x)),该程序有效地要求该函数将该左值作为临时对象处理。 实现可以自由地优化别名检查,如果需要的话可能需要   参数是一个左值。 -end note]

即。如果你说x = std::move(x)那么实现可以优化任何别名检查,例如:

X::operator=(X&& rval) { if (&rval != this) ...

由于实现可以优化该检查,标准库类型甚至不需要首先进行这样的检查。他们只是假设自动分配未定义。

但是,因为自动移动分配可能出现在相当无辜的代码中(可能甚至在用户控制之外,因为std :: lib执行自交换),标准由Defect Report 2468更改。我不认为该DR的解决方案确实有帮助。它不会改变[res.on.arguments]中的任何内容,这意味着执行自动移动分配仍然是未定义的行为,至少在issue 2839得到解决之前。很明显,C ++标准委员会认为自动移动分配不应该导致未定义的行为(即使它们到目前为止在标准中没有实际说明),所以我们的调试模式仍然是a libstdc++ bug包含阻止自动分配的断言。

在我们从libstdc ++中删除过时的检查之前,您可以通过执行此来禁用该单个断言(但仍保留所有其他调试模式检查),然后再包含任何其他标头

#include <debug/macros.h>
#undef __glibcxx_check_self_move_assign
#define __glibcxx_check_self_move_assign(x)

或者等效地,仅使用命令行标志(因此无需更改源代码):

-D_GLIBCXX_DEBUG -include debug/macros.h -U__glibcxx_check_self_move_assign '-D__glibcxx_check_self_move_assign(x)='

这告诉编译器在文件的开头包含<debug/macros.h>,然后取消定义执行self-move-assign断言的宏,然后将其重新定义为空。

(一般来说,定义,取消定义或重新定义libstdc ++的内部宏是未定义和不受支持的,但这样可行,并且有我的祝福。)

答案 2 :(得分:0)

我阅读了一些关于复制构造函数和移动赋值和东西的教程(例如this)。他们都说对象必须检查自我分配,在这种情况下什么都不做。所以我会说这是检查实现的错误,因为自动移动分配非常好。