Re: [PATCH v2 3/4] libstdc++: Specialize _Iter_sink for ostreambuf_iterator

Anlai Lu <[email protected]>
Newsgroups gmane.comp.gcc.patches,gmane.comp.gcc.libstdc++.devel
Message-ID <[email protected]>

> On Aug 18, 2026, at 14:18, Anlai Lu <[email protected]> wrote:
> 
> Hi Tomasz,
> 
> Thanks for the patch.  I have reviewed it previously but just now I ran
> some additional tests.  During testing, I found two issues that should
> be fixed before this is merged.
> 
>> On Aug 11, 2026, at 00:11, Tomasz Kamiński <[email protected]> wrote:
>> 
>> From: Anlai Lu <[email protected]>
>> 
>> Add partial specialization of _Iter_sink for ostreambuf_iterator
>> that inherits _Streambuf_sink, replacing per-character sputc with
>> bulk sputn and zero-copy put-area writes.
>> 
>> All counting and truncation (_M_max) is handled in this
>> specialization so that _Streambuf_sink stays a pure I/O layer.
>> _M_overflow counts all characters and only writes up to the limit,
>> so format_to_n can compute the total output length. _M_discarding
>> returns false for the same reason. Stack writes go through
>> _M_out._M_put() which tracks failure on the iterator.  sputn
>> exceptions are caught and the iterator is marked failed via
>> _M_set_failed().
>> 
>> The maximum count uses size_t with _S_no_limit sentinel, matching
>> the _Ptr_sink convention and avoiding signed comparisons.
>> 
>> libstdc++-v3/ChangeLog:
>> 
>> * include/bits/streambuf_iterator.h (__format::_Iter_sink)
>> [__glibcxx_format]: Declare with specialization for
>> ostreambuf_iterator.
>> (std::ostreambuf_iterator) [__glibcxx_format]: Befriend
>> __format::_Iter_sink for ostreambuf_iterator.
>> * include/std/format (_Iter_sink<_CharT, ostreambuf_iterator<...>>):
>> Define new partial specialization.
>> 
>> Co-authored-by: Tomasz Kamiński <[email protected]>
>> Signed-off-by: Anlai Lu <[email protected]>
>> Signed-off-by: Tomasz Kamiński <[email protected]>
>> ---
>> Changes in v2:
>> - limit unused/reserved buffer to the remaining characters
>>  in _M_overflow/_M_reserve and constructor. Mostly via
>>  calling _M_trim_buf. Rewrote _M_overflow in process
>> - overloads constructor without limit to avoid above
>> - befriend _Iter_sink specialization with ostreambuf_iterator
>> - remove _M_out member and recosntructor iterator in _M_finish
>> 
>> Testing on powerprc64. Tested on x86_64, additionally tested
>> *format* and *print* in all standard modes, assertions and
>> debug. OK for trunk?
>> 
>> .../include/bits/streambuf_iterator.h         | 12 +++
>> libstdc++-v3/include/std/format               | 82 +++++++++++++++++++
>> 2 files changed, 94 insertions(+)
>> 
>> diff --git a/libstdc++-v3/include/bits/streambuf_iterator.h b/libstdc++-v3/include/bits/streambuf_iterator.h
>> index 095928ca4d8..d715edc24ae 100644
>> --- a/libstdc++-v3/include/bits/streambuf_iterator.h
>> +++ b/libstdc++-v3/include/bits/streambuf_iterator.h
>> @@ -42,6 +42,14 @@ namespace std _GLIBCXX_VISIBILITY(default)
>> {
>> _GLIBCXX_BEGIN_NAMESPACE_VERSION
>> 
>> +#ifdef __glibcxx_format // C++ >= 20 && HOSTED
>> +  namespace __format {
>> +    template<typename, typename> class _Iter_sink;
>> +    template<typename _CharT, typename _Traits>
>> +      class _Iter_sink<_CharT, ostreambuf_iterator<_CharT, _Traits>>;
>> +  }
>> +#endif
>> +
>>  /**
>>   * @addtogroup iterators
>>   * @{
>> @@ -266,6 +274,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>> copy(istreambuf_iterator<_CharT2>, istreambuf_iterator<_CharT2>,
>>    ostreambuf_iterator<_CharT2>);
>> 
>> +#ifdef __glibcxx_format // C++ >= 20 && HOSTED
>> +      friend class __format::_Iter_sink<char_type, ostreambuf_iterator>;
>> +#endif
>> +
>>    private:
>>      streambuf_type* _M_sbuf;
>>      bool _M_failed;
>> diff --git a/libstdc++-v3/include/std/format b/libstdc++-v3/include/std/format
>> index 9223cd2e78b..ad6a56ecc3a 100644
>> --- a/libstdc++-v3/include/std/format
>> +++ b/libstdc++-v3/include/std/format
>> @@ -3847,6 +3847,88 @@ namespace __format
>>      }
>>    };
>> 
>> +  // Specialization replacing per-character sputc with bulk sputn
>> +  // and zero-copy writes into the streambuf's put area.
>> +  template<typename _CharT, typename _Traits>
>> +    class _Iter_sink<_CharT, ostreambuf_iterator<_CharT, _Traits>>
>> +    : public _Streambuf_sink<_CharT, _Traits>
>> +    {
>> +      using _Base = _Streambuf_sink<_CharT, _Traits>;
>> +      using _OutIter = ostreambuf_iterator<_CharT, _Traits>;
>> +
>> +      static constexpr size_t _S_no_limit = size_t(-1);
>> +
>> +      void
>> +      _M_trim_buf()
>> +      {
>> + const size_t __avail = _M_max - _M_count;
>> + if (this->_M_unused().size() > __avail)
>> +  this->_M_reset(this->_M_unused().first(__avail));
>> +      }
>> +
>> +    protected:
>> +      size_t _M_max = _S_no_limit;
>> +      size_t _M_count = 0;
>> +
>> +      void
>> +      _M_overflow() override
>> +      {
>> + const size_t __new = this->_M_used().size();
>> + if (_M_count >= _M_max)
>> +  this->_M_reset(this->_M_buf);
>> + else if (size_t __a = _M_max - _M_count; __a > __new)
>> +  {
>> +    _Base::_M_overflow();
>> +    _M_trim_buf();
> 
> _M_trim_buf() depends on the value of _M_count.  In this else-if
> branch it is called before _M_count is updated, which leads to an
> incorrect trim.
> 
>> +  }
>> + else
>> +  {
>> +    this->_M_flush();
>> +    this->_M_reset(this->_M_buf);
>> +  }
>> + _M_count += __new;
>> +      }
>> +
>> +      bool
>> +      _M_discarding() const override
>> +      { return false; }
>> +
>> +    public:
>> +      [[__gnu__::__always_inline__]]
>> +      explicit
>> +      _Iter_sink(_OutIter __out)
>> +      : _Base(__out._M_sbuf)
>> +      { }
>> +
>> +      [[__gnu__::__always_inline__]]
>> +      explicit
>> +      _Iter_sink(_OutIter __out, iter_difference_t<_OutIter> __max)
>> +      : _Base(__out._M_sbuf), _M_max(__max < 0 ? _S_no_limit : size_t(__max))
>> +      { _M_trim_buf(); }
>> +
>> +      typename _Sink<_CharT>::_Reservation
>> +      _M_reserve(size_t __n) override
>> +      {
>> + if (_M_count < _M_max)
>> +  if (size_t __a = _M_max - _M_count; __a >= __n)
>> +    return _Base::_M_reserve(__n);
>> + return { nullptr };
>> +      }
>> +
>> +      format_to_n_result<_OutIter>
>> +      _M_finish() &&
>> +      {
>> + _M_count += this->_M_used().size();
>> + if (_M_count < _M_max)
>> +  this->_M_flush();
> 
> This condition should be _M_count <= _M_max.
> When _M_count becomes exactly _M_max, there may still be data
> that has not been flushed.
> 
>> +
>> + _OutIter __out;
>> + __out._M_sbuf = this->_M_sbuf;
>> + __out._M_failed = this->_M_write_failed;
>> + return { std::move(__out), ptrdiff_t(_M_count) };
>> +      }
>> +    };
>> +
>>  // Used for contiguous iterators.
>>  // No buffer is used, characters are written straight to the iterator.
>>  // We do not know the size of the output range, so the span size just grows
>> -- 
>> 2.55.0
> 
> Two issues above can be covered by this test case:
> 
>  std::string big(257, 'A');
>  std::ostringstream os;
>  auto res = std::format_to_n(std::ostreambuf_iterator<char>(os),
>                              257, "{}", big);
>  VERIFY(res.size == 257);
>  VERIFY(os.view() == big);
> 
> The existing test suite does not cover this exact-boundary case, which
> is why these issues were not caught earlier.  The existing cases in
> test_format_to_n have a (len=1000, limit=256) and (len=8, limit=8), but
> does not cover the case (len > 256 && len = limit).

Quick follow up:

I previously wrote that the (len=8, limit=8) test passes because 8 < 256.
That is not the full picture.  The real reason is that the len=limit=8
case uses {}{}, "AAAA", "BBBB", which has intermediate flush.

The second issue can be found with just
  std::format_to_n(oit, 1, "{}", "A");

Sorry for the confusion in my earlier analysis.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.