https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104222

            Bug ID: 104222
           Summary: std::basic_string::resize_and_overwrite passes an
                    unexpected value to user's callable
           Product: gcc
           Version: 12.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: space.mission at yandex dot ru
  Target Milestone: ---

TOPIC:
    A bug in std::basic_string::resize_and_overwrite, N4901 / P1072R10.

DESCRIPTION:
    A value `n` passed to std::basic_string::resize_and_overwrite(n, op) MAY
NOT be equal (in some circumstances) to the value passed to the user's callable
`op`, despite what is implied in [string.capacity] N4901, p.785.

EXPECTED BEHAVIOR:
    A value `n` passed to std::basic_string::resize_and_overwrite(n, op) shall
ALWAYS be equal to the value passed to the user's callable `op`.

COMPILER VERSION:
    "x86-64 gcc (trunk) - now is 12.0.1" on godbolt.org

HOW to reproduce: [https://godbolt.org/z/j9K6zYq83]
g++ -std=c++2b (... other options are irrelevant)

#include <cassert>
#include <string>
int main() {
    std::string s(30, '_');
    s.resize_and_overwrite(40, [](char*, int n) {
    //  assert( n == 60 ); // Passes, but should not!
        assert( n == 40 ); // Fails, but should not!!
        return n;
    });
}

Prints: /example.cpp:7: main()::<lambda(char*, int)>: Assertion `n == 40'
failed.


NOTE:
    libc++ passes the assertion `assert( n == 40 )' in the code snippet above
if executed as "clang++ -std=c++2b -stdlib=libc++". See
[https://godbolt.org/z/1YET3csh4].

NOTE:
    Existing tests in
[libstdc++-v3/testsuite/21_strings/basic_string/capacity/char/resize_and_overwrite.cc],
e.g. test01(), test03() do NOT catch this bug because the condition for it to
happen is not met.

DETAILS:
    If the value `n` passed to s.resize_and_overwrite(n, op) is such that the
inequality `s.capacity() < n < 2 * s.capacity()` holds, the value __n that is
passed to user's _Operation op(char*, __n) NOT equals to `n`, but equals to the
new capacity (after extension of the internal string's buffer to that new
capacity).

ANALYSIS:
    See _M_create(size_type&, size_type): the first param is ref (in-out) for a
new capacity. The call to it in "basic_string.tcc" is: _M_create(__n,
__capacity). This call modifies the __n, IFF __capacity < __n < 2 * __capacity.
Otherwise __n stays unmodified (it is still used as a parameter for the new
capacity, but in such case the new capacity becomes equal to __n) and the
problem does not occur.

FIXING PATCH:
    Just add an additional variable (e.g. __new_capacity) to prevent changing
the __n by _M_create) in
   
[https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/include/bits/basic_string.tcc]

    Relative to
[https://github.com/gcc-mirror/gcc/blob/0d56eb93aa6e58328fbf679a4839bfaef5c05f5c/libstdc%2B%2B-v3/include/bits/basic_string.tcc#L565]:
...
        resize_and_overwrite(size_type __n, _Operation __op)
        {
            const size_type __capacity = capacity();
            _CharT* __p;
            if (__n > __capacity)
            {
--              __p = _M_create(__n, __capacity);
++              auto __new_capacity = __n;
++              __p = _M_create(__new_capacity, __capacity);
                this->_S_copy(__p, _M_data(), length()); // exclude trailing
null
    #if __cpp_lib_is_constant_evaluated
                if (std::is_constant_evaluated())
                    traits_type::assign(__p + length(), __n - length(),
_CharT());
    #endif
                _M_dispose();
                _M_data(__p);
--              _M_capacity(__n);
++              _M_capacity(__new_capacity);
            }
...


NEW TEST:
for
[https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/testsuite/21_strings/basic_string/capacity/char/resize_and_overwrite.cc]
    // To catch the bug the value n ("resize to <= n") should satisfy:
    //     current_capacity < n < 2*current_capacity,
    std::string s(30, '_');                         // size == capacity == 30
    s.resize_and_overwrite(40, [](char*, int n) {   // resize to <= 40
      VERIFY( n == 40 );                            // capacity now == 2*30
      return n;
    });

REFERENCES:
    [http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1072r10.html]
P1072R10 - basic_string::resize_and_overwrite
  • [Bug libstdc++/104222] New: st... space.mission at yandex dot ru via Gcc-bugs

Reply via email to