Bug 85828

Summary: std::shuffle tries to swap element with itself
Product: gcc Reporter: David Seifert <soap>
Component: libstdc++Assignee: Not yet assigned to anyone <unassigned>
Status: NEW ---    
Severity: normal CC: webrown.cpp
Priority: P3    
Version: 7.3.0   
Target Milestone: ---   
URL: https://stackoverflow.com/questions/22915325/avoiding-self-assignment-in-stdshuffle
Host: Target:
Build: Known to work:
Known to fail: Last reconfirmed: 2018-05-18 00:00:00

Description David Seifert 2018-05-18 12:45:34 UTC
This bug is related to a previous one in https://gcc.gnu.org/bugzilla/show_bug.cgi?id=59603. For a full description, see https://stackoverflow.com/questions/22915325/avoiding-self-assignment-in-stdshuffle. The problem boils down to the fact that in std::shuffle, self-assignment in some std::swap(x, x) will happen eventually. This will cause an "Error: attempt to self move assign." failure to occur and kill the program when compiled in debug mode. A chat on IRC with Jonathan Wakely confirmed that this is an implementation bug, and that the likely correct fix for this is to avoid self-swap in std::shuffle.
Comment 1 Jonathan Wakely 2018-05-18 12:52:58 UTC
#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());
}

$ g++ self.cc -D_GLIBCXX_DEBUG
$ ./a.out
/usr/include/c++/7/debug/safe_container.h:83:
Error: attempt to self move assign.

Objects involved in the operation:
    sequence "this" @ 0x0x12e7ee8 {
      type = __gnu_debug::_Safe_container<std::__debug::vector<int, std::allocator<int> >, std::allocator<int>, __gnu_debug::_Safe_sequence, true>;
    }
Aborted (core dumped)
Comment 2 Jonathan Wakely 2018-05-18 15:39:31 UTC
This is ugly but it works:

--- a/libstdc++-v3/include/bits/stl_algo.h
+++ b/libstdc++-v3/include/bits/stl_algo.h
@@ -3827,7 +3827,9 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
        if ((__urange % 2) == 0)
        {
          __distr_type __d{0, 1};
-         std::iter_swap(__i++, __first + __d(__g));
+         if (__d(__g) == 0)
+           std::iter_swap(__i, __first);
+         ++__i;
        }
 
        // Now we know that __last - __i is even, so we do the rest in pairs,
@@ -3841,8 +3843,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
          const pair<__uc_type, __uc_type> __pospos =
            __gen_two_uniform_ints(__swap_range, __swap_range + 1, __g);
 
-         std::iter_swap(__i++, __first + __pospos.first);
-         std::iter_swap(__i++, __first + __pospos.second);
+         _RandomAccessIterator __pos1 = __first + __pospos.first;
+         _RandomAccessIterator __pos2 = __first + __pospos.second;
+         if (__i != __pos1)
+           std::iter_swap(__i, __pos1);
+         ++__i;
+         if (__i != __pos2)
+           std::iter_swap(__i, __pos2);
+         ++__i;
        }
 
        return;


I think we also want to remove the __glibcxx_check_self_move_assign assertions completely.
Comment 3 GCC Commits 2020-08-12 19:37:24 UTC
The master branch has been updated by Jonathan Wakely <redi@gcc.gnu.org>:

https://gcc.gnu.org/g:c2fb0a1a2e7a0fb15cf3cf876f621902ccd273f0

commit r11-2677-gc2fb0a1a2e7a0fb15cf3cf876f621902ccd273f0
Author: Jonathan Wakely <jwakely@redhat.com>
Date:   Wed Aug 12 20:36:00 2020 +0100

    libstdc++: Make self-move well-defined for containers [PR 85828]
    
    The C++ LWG recently confirmed that self-move assignment should not have
    undefined behaviour for standard containers (see the proposed resolution
    of LWG 2839). The result should be a valid but unspecified value, just
    like other times when a container is moved from.
    
    Our std::list, std::__cxx11::basic_string and unordered containers all
    have bugs which result in undefined behaviour.
    
    For std::list the problem is that we clear the previous contents using
    _M_clear() instead of clear(). This means the _M_next, _M_prev and
    _M_size members are not zeroed, and so after we "update" them (with
    their existing values), we are left with dangling pointers and a
    non-zero size, but no elements.
    
    For the unordered containers the problem is similar. _Hashtable first
    deallocates the existing contents, then takes ownership of the pointers
    from the RHS object (which has just had its contents deallocated so the
    pointers are dangling).
    
    For std::basic_string it's a little more subtle. When the string is
    local (i.e. fits in the SSO buffer) we use char_traits::copy to copy the
    contents from this->data() to __rhs.data(). When &__rhs == this that
    copy violates the precondition that the ranges don't overlap. We only
    need to check for self-move for this case where it's local, because the
    only other case that can be true for self-move is that it's non-local
    but the allocators compare equal. In that case the data pointer is
    neither deallocated nor leaked, so the result is well-defined.
    
    This patch also makes a small optimization for std::deque move
    assignment, to use the efficient move when is_always_equal is false, but
    the allocators compare equal at runtime.
    
    Finally, we need to remove all the Debug Mode checks which abort the
    program when a self-move is detected, because it's not undefined to do
    that.
    
    Before PR 85828 can be closed we should also look into fixing
    std::shuffle so it doesn't do any redundant self-swaps.
    
    libstdc++-v3/ChangeLog:
    
            PR libstdc++/85828
            * include/bits/basic_string.h (operator=(basic_string&&)): Check
            for self-move before copying with char_traits::copy.
            * include/bits/hashtable.h (operator=(_Hashtable&&)): Check for
            self-move.
            * include/bits/stl_deque.h (_M_move_assign1(deque&&, false_type)):
            Check for equal allocators.
            * include/bits/stl_list.h (_M_move_assign(list&&, true_type)):
            Call clear() instead of _M_clear().
            * include/debug/formatter.h (__msg_self_move_assign): Change
            comment.
            * include/debug/macros.h (__glibcxx_check_self_move_assign):
            (_GLIBCXX_DEBUG_VERIFY): Remove.
            * include/debug/safe_container.h (operator=(_Safe_container&&)):
            Remove assertion check for safe move and make it well-defined.
            * include/debug/safe_iterator.h (operator=(_Safe_iterator&&)):
            Remove assertion check for self-move.
            * include/debug/safe_local_iterator.h
            (operator=(_Safe_local_iterator&&)): Likewise.
            * testsuite/21_strings/basic_string/cons/char/self_move.cc: New test.
            * testsuite/23_containers/deque/cons/self_move.cc: New test.
            * testsuite/23_containers/forward_list/cons/self_move.cc: New test.
            * testsuite/23_containers/list/cons/self_move.cc: New test.
            * testsuite/23_containers/set/cons/self_move.cc: New test.
            * testsuite/23_containers/unordered_set/cons/self_move.cc: New test.
            * testsuite/23_containers/vector/cons/self_move.cc: New test.