On Mon, Jul 6, 2026 at 11:40 AM Tomasz Kaminski <[email protected]> wrote:

>
>
> On Mon, Jul 6, 2026 at 6:28 AM Anlai Lu <[email protected]> wrote:
>
>> Introduce _Streambuf_sink that writes directly to basic_streambuf
>> via sputn, preferring zero-copy writes into the streambuf put area
>> (pptr/epptr/pbump) and falling back to the stack buffer and bulk
>> sputn.
>>
>> libstdc++-v3/ChangeLog:
>>
>>         * include/bits/streambuf_iterator.h (ostreambuf_iterator):
>>         Add internal _M_get_sbuf() and _M_set_failed() members.
>>         * include/std/format: Include <bits/streambuf_iterator.h>.
>>         (__format::_Streambuf_sink): New class template.
>>         * include/std/streambuf: Forward-declare
>> __format::_Streambuf_sink,
>>         add friend declaration to basic_streambuf.
>>
>> Signed-off-by: Anlai Lu <[email protected]>
>> ---
>>  .../include/bits/streambuf_iterator.h         |  11 ++
>>  libstdc++-v3/include/std/format               | 163 ++++++++++++++++++
>>  libstdc++-v3/include/std/streambuf            |   4 +
>>  3 files changed, 178 insertions(+)
>>
>> diff --git a/libstdc++-v3/include/bits/streambuf_iterator.h
>> b/libstdc++-v3/include/bits/streambuf_iterator.h
>> index 095928ca4..d919fdbb8 100644
>> --- a/libstdc++-v3/include/bits/streambuf_iterator.h
>> +++ b/libstdc++-v3/include/bits/streambuf_iterator.h
>> @@ -318,6 +318,17 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>        failed() const _GLIBCXX_USE_NOEXCEPT
>>        { return _M_failed; }
>>
>> +      /// @cond internal
>> +      _GLIBCXX_NODISCARD
>> +      streambuf_type*
>> +      _M_get_sbuf() const _GLIBCXX_USE_NOEXCEPT
>> +      { return _M_sbuf; }
>> +
>> +      void
>> +      _M_set_failed() _GLIBCXX_USE_NOEXCEPT
>> +      { _M_failed = true; }
>> +      /// @endcond
>> +
>>        ostreambuf_iterator&
>>        _M_put(const _CharT* __ws, streamsize __len)
>>        {
>> diff --git a/libstdc++-v3/include/std/format
>> b/libstdc++-v3/include/std/format
>> index dd275b6b8..a449efd45 100644
>> --- a/libstdc++-v3/include/std/format
>> +++ b/libstdc++-v3/include/std/format
>> @@ -60,6 +60,7 @@
>>  #include <bits/ranges_algobase.h> // ranges::copy
>>  #include <bits/stl_iterator.h> // counted_iterator
>>  #include <bits/stl_pair.h>     // __is_pair
>> +#include <bits/streambuf_iterator.h> // ostreambuf_iterator,
>> basic_streambuf
>>  #include <bits/unicode.h>      // __is_scalar_value, _Utf_view, etc.
>>  #include <bits/utility.h>      // tuple_size_v
>>  #include <ext/numeric_traits.h> // __int_traits
>> @@ -3531,6 +3532,168 @@ namespace __format
>>        { }
>>      };
>>
>> +  // A format sink that writes directly to a basic_streambuf.
>> +  // Prefers zero-copy writes into the streambuf's put area
>> +  // (pptr/epptr/pbump), falling back to the stack buffer
>> +  // (_M_buf) and bulk sputn.
>> +  // _M_count tracks chars output (for _M_max truncation decisions);
>> +  // _M_total tracks all chars formatted (for format_to_n_result::size).
>> +  template<typename _CharT, typename _Traits = char_traits<_CharT>>
>> +    class _Streambuf_sink : public _Buf_sink<_CharT>
>> +    {
>> +      using _Buf_sink<_CharT>::_M_buf;
>> +      using __diff_t
>> +       = iter_difference_t<ostreambuf_iterator<_CharT, _Traits>>;
>> +      enum class _Sink_state { _S_stack, _S_put_area, _S_failed };
>>
>
>
>> +
>> +      basic_streambuf<_CharT, _Traits>* _M_sbuf;
>> +      __diff_t _M_max = -1;
>> +      __diff_t _M_count = 0;
>> +      __diff_t _M_total = 0;
>>
> I do not think we need separate _M_count and _M_total,
> we can just check if _M_total > _M_count.
>
>> +      _Sink_state _M_state = _Sink_state::_S_stack;
>> +
>> +      void
>> +      _M_use_stackbuf()
>> +      {
>> +       this->_M_reset(_M_buf);
>> +       if (_M_state != _Sink_state::_S_failed)
>> +         _M_state = _Sink_state::_S_stack;
>> +      }
>> +
>> +    protected:
>> +      // Try to switch to the streambuf's put area for zero-copy writes.
>> +      bool
>> +      _M_use_put_area(size_t __n)
>> +      {
>> +       if (_M_has_failed())
>> +         return false;
>> +       if (auto __p = _M_sbuf->pptr())
>> +         if (auto __e = _M_sbuf->epptr();
>> +             __e && __e > __p
>>
> +             && static_cast<size_t>(__e - __p) >= __n)
>> +           {
>> +             size_t __avail = static_cast<size_t>(__e - __p);
>> +             if (_M_max >= 0)
>> +               {
>> +                 auto __limit = _M_max - _M_count;
>> +                 if (__limit <= 0
>> +                     || static_cast<size_t>(__limit) < __n)
>> +                   return false;
>> +                 if (__avail > static_cast<size_t>(__limit))
>> +                   __avail = static_cast<size_t>(__limit);
>> +               }
>> +             this->_M_reset(span<_CharT>{__p, __avail});
>> +             _M_state = _Sink_state::_S_put_area;
>> +             return true;
>> +           }
>> +       return false;
>> +      }
>> +
>> +      // Write __s to the streambuf via sputn (stack buffer)
>> +      // or pbump (put area), depending on _M_state.
>> +      void
>> +      _M_do_flush(span<_CharT> __s)
>> +      {
>> +       _M_total += __s.size();
>> +       if (_M_max >= 0)
>> +       {
>> +         auto __limit = _M_max - _M_count;
>> +         if (__limit <= 0)
>> +           return;
>> +         if (__s.size() > static_cast<size_t>(__limit))
>> +           __s = __s.first(static_cast<size_t>(__limit));
>> +       }
>> +
>> +       switch (_M_state)
>> +       {
>> +         case _Sink_state::_S_stack:
>> +           {
>> +             streamsize __written
>> +               = _M_sbuf->sputn(__s.data(), __s.size());
>> +             if (__written != __s.size()) [[unlikely]]
>> +               _M_state = _Sink_state::_S_failed;
>> +             _M_count += __written;
>> +           }
>> +           break;
>> +         case _Sink_state::_S_put_area:
>> +           _M_sbuf->__safe_pbump(__s.size());
>> +           _M_count += __s.size();
>> +           break;
>> +         case _Sink_state::_S_failed:
>> +           break;
>> +       }
>> +      }
>> +
>> +      void
>> +      _M_overflow() override
>> +      {
>>
> // I assume we do not handle counts on this level for these comments.
> // I think we should check if we are not failed (discarding), and simply
> // rewind the buffer in this case.
>
>> +       auto __s = this->_M_used();
>> +       if (!__s.empty()) [[likely]]
>> +         _M_do_flush(__s);
>> +       if (_M_max >= 0 && _M_count >= _M_max)
>> +         {
>> +           _M_use_stackbuf();
>> +           return;
>> +         }
>> +       if (!_M_use_put_area(0))
>> +         _M_use_stackbuf();
>> +      }
>> +
>> +      bool
>> +      _M_discarding() const override
>> +      {
>> +       return _M_state == _Sink_state::_S_failed
>> +         || (_M_max >= 0 && _M_count >= _M_max);
>>
> Unfortunately, we cannot override the _M_discarding for the sink that
> is used for format_to_n, and needs to return the total lenght of the
> hypothetical output. Because, if _M_discarding is true the formatter is
> expected to stop emitting the output, and thus preventing the count to
> be fully computed.
>
We do not yet use _M_discarding, but my plan was to use it in
range_formatter,
to stop printing sequence elements. Adding this method (as any new virtual
method)
would be ABI break later.

>
> I think we could still combine both, by moving all the counters to
> iter_sink
> specialization. This why if we have use _Streambuf_sink for format_to
> we could provide _M_discarding.
>
> The iteration specialization, can still see all characters, by intercepting
> _M_overflow and _M_bump, and counting the characters and adjusting
> _M_used to be withing the limit. I would not reduce the buffer size,
> let the formatter fill the buffer, and just adjust _M_bump.
>
>> +      }
>> +
>> +    public:
>> +      explicit
>> +      _Streambuf_sink(basic_streambuf<_CharT, _Traits>* __sbuf,
>> +                     __diff_t __max = -1) noexcept
>> +      : _M_sbuf(__sbuf), _M_max(__max)
>> +      { }
>> +
>> +      using _Sink<_CharT>::out;
>> +
>> +      typename _Sink<_CharT>::_Reservation
>> +      _M_reserve(size_t __n) override
>>
> I believe that you also need to override _M_bump, to "commit"
> the written characters.
>
>> +      {
>> +       if (_M_has_failed())
>> +         return { nullptr };
>> +
>> +       if (!this->_M_used().empty())
>> +         {
>> +           if(__n <= this->_M_unused().size())
>> +             return { this };
>> +
>> +           this->_M_do_flush(this->_M_used());
>> +         }
>> +
>> +       // Try to write directly into the streambuf's put area.
>> +       if (_M_use_put_area(__n))
>> +         return { this };
>> +
>> +       // Otherwise reset to the stack buffer.
>> +       _M_use_stackbuf();
>> +       if (__n <= this->_M_unused().size())
>> +         return { this };
>> +
>> +       return { nullptr };
>> +      }
>> +
>> +      bool
>> +      _M_has_failed() const noexcept
>> +      { return _M_state == _Sink_state::_S_failed; }
>> +
>> +      void
>> +      _M_set_failed() noexcept
>> +      { _M_state = _Sink_state::_S_failed; }
>> +
>> +      __diff_t
>> +      _M_total_chars() const noexcept
>> +      { return _M_total; }
>> +    };
>> +
>>    using _GLIBCXX_STD_C::vector;
>>
>>    // A sink that fills a sequence (e.g. std::string, std::vector,
>> std::deque).
>> diff --git a/libstdc++-v3/include/std/streambuf
>> b/libstdc++-v3/include/std/streambuf
>> index 616e44f74..22b505228 100644
>> --- a/libstdc++-v3/include/std/streambuf
>> +++ b/libstdc++-v3/include/std/streambuf
>> @@ -57,6 +57,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>      __copy_streambufs_eof(basic_streambuf<_CharT, _Traits>*,
>>                           basic_streambuf<_CharT, _Traits>*, bool&);
>>
>> +  namespace __format { template<typename, typename> class
>> _Streambuf_sink; }
>> +
>>    /**
>>     *  @brief  The actual work of input and output (interface).
>>     *  @ingroup io
>> @@ -149,6 +151,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>        friend class basic_ostream<char_type, traits_type>;
>>        friend class istreambuf_iterator<char_type, traits_type>;
>>        friend class ostreambuf_iterator<char_type, traits_type>;
>> +      template<typename, typename>
>> +       friend class __format::_Streambuf_sink;
>>
>>        friend streamsize
>>        __copy_streambufs_eof<>(basic_streambuf*, basic_streambuf*, bool&);
>> --
>> 2.34.1
>>
>>

Reply via email to