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