Re: [PATCH 2/4] libstdc++: Add _Streambuf_sink for direct streambuf formatting

Tomasz Kaminski <[email protected]>
Newsgroups gmane.comp.gcc.patches,gmane.comp.gcc.libstdc++.devel
Message-ID <CAKvuMXAX6OSn75YLgAwDwx4vaijeqc0sZLLT+zD=Ku60RJEPpw@mail.gmail.com>
On Thu, Jul 16, 2026 at 2:04 PM 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.
>
> Streambuf I/O errors are tracked internally (_M_write_failed) and
> reported after formatting completes, so that format-phase exceptions
> (format_error, bad_alloc) propagate without setting badbit per
> [ostream.formatted.print]/(4.2).
>
> The _Sink_state enum is omitted in favor of _M_on_stack() which
> tests whether the current span points at the stack buffer.
>
> 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               | 132 ++++++++++++++++++
>  libstdc++-v3/include/std/streambuf            |   4 +
>  3 files changed, 147 insertions(+)
>
I will post updated patch with changes made, but makign some commit.

>
> 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
> +
>
This should go to next patch, and we should befriend the _Iter_sink
specialization
instead.

>        ostreambuf_iterator&
>        _M_put(const _CharT* __ws, streamsize __len)
>        {
> diff --git a/libstdc++-v3/include/std/format
> b/libstdc++-v3/include/std/format
> index 9bf5e7857..b676629b1 100644
> --- a/libstdc++-v3/include/std/format
> +++ b/libstdc++-v3/include/std/format
> @@ -59,6 +59,7 @@
>  #include <bits/ranges_util.h>  // subrange
>  #include <bits/ranges_algobase.h> // ranges::copy
>  #include <bits/stl_iterator.h> // counted_iterator
> +#include <bits/streambuf_iterator.h> // ostreambuf_iterator,
> basic_streambuf
>  #include <bits/stl_pair.h>     // __is_pair
>  #include <bits/unicode.h>      // __is_scalar_value, _Utf_view, etc.
>  #include <bits/utility.h>      // tuple_size_v
> @@ -3530,6 +3531,137 @@ 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.
> +  template<typename _CharT, typename _Traits = char_traits<_CharT>>
> +    class _Streambuf_sink : public _Buf_sink<_CharT>
> +    {
> +      using _Buf_sink<_CharT>::_M_buf;
> +
> +    protected:
> +      basic_streambuf<_CharT, _Traits>* _M_sbuf;
> +      bool _M_write_failed = false;
> +
> +      _GLIBCXX_CONSTEXPR_FORMAT bool
> +      _M_on_stack() const noexcept
> +      { return this->_M_used().data() == this->_M_buf; }
> +
> +      // Switch to _M_buf when the streambuf has no put area
> +      // or we need to stop using it.
> +      _GLIBCXX_CONSTEXPR_FORMAT void
> +      _M_use_stackbuf()
> +      { this->_M_reset(_M_buf); }
> +
> +      // Try to point our span directly into the streambuf's put
> +      // area for zero-copy writes.  Caller must fall back to
> +      // _M_use_stackbuf() on failure.
> +      _GLIBCXX_CONSTEXPR_FORMAT bool
> +      _M_use_put_area(size_t __n = 0)
> +      {
> +       if (auto __p = _M_sbuf->pptr())
> +         {
> +           auto __e = _M_sbuf->epptr();
> +           if (__e && __e > __p
> +               && static_cast<size_t>(__e - __p) >= __n)
>
We could use some if with initializer here.

> +             {
> +               this->_M_reset(
> +                 span<_CharT>{__p, static_cast<size_t>(__e - __p)});
> +               return true;
> +             }
> +         }
> +       return false;
> +      }
> +
> +      // Allow derived classes to commit to the put area
> +      // without needing friendship to basic_streambuf.
> +      _GLIBCXX_CONSTEXPR_FORMAT void
> +      _M_pbump(streamsize __n)
> +      { _M_sbuf->__safe_pbump(__n); }
> +
> +      // Write characters to the streambuf, tracking errors.
> +      // I/O exceptions from sputn are caught and converted to
> +      // _M_write_failed so they do not propagate as vformat exceptions
> +      // per [ostream.formatted.print]/(4.2).  Does not refresh the
> +      // span; callers use _M_use_put_area/stackbuf.
> +      _GLIBCXX_CONSTEXPR_FORMAT void
> +      _M_flush(span<_CharT> __s)
> +      {
> +       if (__s.empty() || _M_write_failed) [[unlikely]]
> +         return;
> +       __try {
> +         if (_M_on_stack())
> +           {
> +             if (_M_sbuf->sputn(__s.data(), __s.size())
> +                 != static_cast<streamsize>(__s.size()))
> +               _M_write_failed = true;
> +           }
> +         else
> +           _M_pbump(__s.size());
> +       }
> +       __catch(const __cxxabiv1::__forced_unwind&)
> +         { throw; }
> +       __catch(...)
> +         { _M_write_failed = true; }
> +      }
> +
> +      _GLIBCXX_CONSTEXPR_FORMAT void
>
This should not be cosntexpr, as they cannot be never used at compile time.

> +      _M_overflow() override
> +      {
> +       _M_flush(this->_M_used());
> +       if (!_M_use_put_area())
> +         _M_use_stackbuf();
> +      }
> +
> +    public:
> +      [[__gnu__::__always_inline__]]
> +      _GLIBCXX_CONSTEXPR_FORMAT explicit
> +      _Streambuf_sink(basic_streambuf<_CharT, _Traits>* __sbuf) noexcept
> +      : _M_sbuf(__sbuf)
> +      { }
>
This is important part, we are currently allocating the buffer on stack
here,
and we will copy it at least once (this hid some bugs in _Iter_sink, when we
allowed it to write more than n characters). We should instead
call _M_use_put_area()
in constructor, so we achieve true zero-copy here, by writting and
reserving directly
from it.

> +
> +      using _Sink<_CharT>::out;
> +
> +      // _M_bump is not overridden: the default advances _M_next within
> +      // the current span (stack buffer or put area).  Actual commit
> +      // to the streambuf happens via _M_flush, called from _M_overflow
> +      // (buffer full) or _M_finish (end of formatting).
> +
> +      _GLIBCXX_CONSTEXPR_FORMAT typename _Sink<_CharT>::_Reservation
> +      _M_reserve(size_t __n) override
> +      {
> +       if (__n <= this->_M_unused().size())
> +         return { this };
> +
> +       if (!this->_M_used().empty())
> +         _M_overflow();
> +
> +       // 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 };
> +      }
> +
> +      _GLIBCXX_CONSTEXPR_FORMAT bool
> +      _M_discarding() const override
> +      { return _M_write_failed; }
> +
> +      _GLIBCXX_CONSTEXPR_FORMAT void
> +      _M_finish() &&
> +      { _M_flush(this->_M_used()); }
> +
> +      _GLIBCXX_CONSTEXPR_FORMAT bool
> +      _M_failed() const noexcept
> +      { return _M_write_failed; }
> +    };
> +
>    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
>
>
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.