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