C++11 避免std::shuffle中的自赋值

C++11 避免std::shuffle中的自赋值,c++11,g++,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 &l

在使用检查过的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 = {{{1}}, {{1, 2}}};
        std::shuffle(intVectors.begin(), intVectors.end(), std::mt19937());
}
由于
类型
没有显式地给出
运算符=(&&&&)
,因此默认情况下,它是通过“递归地”对其成员应用相同的操作来实现的

问题发生在交换代码的第2行,其中
\uuuuu a
\uuuu b
指向相同的对象,导致代码
\uuuu a.operator=(std::move(\uu a))
生效,然后触发选中的
向量::operator=(&&&&)
实现中的错误

我的问题是:这是谁的错

  • 它是我的吗?因为我应该为
    swap
    提供一个实现,使“自交换”成为
    NOP
  • 是不是因为它不应该尝试与自身交换一个元素而使用std::shuffle
  • 是否是检查过的实现,因为自移动分配是完全正确的
  • 一切都是正确的,检查过的实现只是帮了我一个忙,让我做这个额外的检查(但是如何关闭它)
我读过关于shuffle要求迭代器是可值交换的。这是否扩展到了自交换(这仅仅是一个运行时问题,不能通过编译时概念检查来实现)

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

#include <vector>

int main() {
    std::vector<int> vectorOfInts;
    vectorOfInts = std::move(vectorOfInts);
}
#包括
int main(){
std::向量向量;
vectorOfInts=std::move(vectorOfInts);
}
当然,这是很明显的(为什么要将向量移动到自身?)。
如果您直接在其中交换
std::vector
s,则不会发生错误,因为vector类有一个交换函数的自定义实现,该函数不使用
运算符=(&&&)

我读了一些关于复制构造函数和移动赋值等内容的教程(例如)。他们都说对象必须检查自我分配,在这种情况下什么也不做。因此,我想说这是checked实现的错误,因为self-move-assignment非常好。

这是GCC checked实现中的一个bug。根据,可交换的要求包括(重点):

17.6.3.2§4当且仅当
t
分别可与
t
类型的任何右值或左值交换时,右值或左值
t
才可交换

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

20.2.2§2要求:类型
T
应可移动(表20)和可移动(表22)

因此,要在默认交换运算符的定义下可交换,自移动分配必须有效,并且具有后条件,即自分配后的
t
等于其旧值(但不一定是无操作!),如表22所示


虽然您要交换的对象不是标准类型,但MoveAssignable没有先决条件,即
rv
t
引用不同的对象,并且只要所有成员都是可移动分配的(如
std::vector
应该是),则生成移动分配运算符必须正确(根据12.8§29执行会员移动分配时)。此外,尽管注释中指出,
rv
具有有效但未指定的状态,但任何状态(除了与其原始值相等)对于自赋值都是不正确的,否则将违反后条件。

libstdc++调试模式断言基于标准中的此规则,来自[res.on.arguments]

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

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

该段还包含本说明(重点):

[注意:如果程序将左值强制转换为xvalue,同时将该左值传递给库函数(例如,通过使用参数调用函数
std::move(x)
),程序有效地要求该函数将该左值视为临时对象。该实现可自由优化消除别名检查,如果 参数是左值。-结束注释]

i、 e.如果您说
x=std::move(x)
,那么实现可以优化任何别名检查,例如:

X::operator=(X&& rval) { if (&rval != this) ...
由于实现可以优化这种检查,标准库类型一开始甚至不必费心做这样的检查,它们只是假设未定义自移动分配

但是,由于自移动分配可能出现在非常无辜的代码中(甚至可能超出用户的控制范围,因为std::lib执行自交换),因此标准被更改。不过,我认为DR的解决方案实际上没有帮助。它不会改变[res.on.arguments]中的任何内容这意味着执行自我移动任务仍然是未定义的行为,至少在解决之前。显然,C++标准委员会认为自移分配不应该导致未定义的行为(即使他们迄今为止还没有在标准中这样说)。因此,我们的调试模式仍然包含防止自移动分配的断言

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

<
X::operator=(X&& rval) { if (&rval != this) ...
#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)='