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

Anlai Lu agicy@qq.com
Tue Aug 18 07:10:51 GMT 2026



> On Aug 18, 2026, at 14:18, Anlai Lu <agicy@qq.com> 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 <tkaminsk@redhat.com> wrote:
>> 
>> From: Anlai Lu <agicy@qq.com>
>> 
>> 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 <tkaminsk@redhat.com>
>> Signed-off-by: Anlai Lu <agicy@qq.com>
>> Signed-off-by: Tomasz Kamiński <tkaminsk@redhat.com>
>> ---
>> 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.



More information about the Libstdc++ mailing list