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.
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
>
>