[v4,2/4] libstdc++: Add _Streambuf_sink for direct streambuf formatting
Commit Message
From: Anlai Lu <agicy@qq.com>
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 _M_write_failed is set also
when empty streambuf pointer is provided, as this allow correctly
propagating failed flag for default constructed ostreambuf_iterator.
The _Streambuf_sink is specialized only for char_traits<_CharT>,
as only in that case we are guaranteed that __safe_pbump member is
present.
libstdc++-v3/ChangeLog:
* include/std/format (__format::_Streambuf_sink): New class
template.
* include/std/streambuf: Include bits/version.h.
(__format::_Streambuf_sink) [__glibcxx_format]: Declare.
(std::basic_streambuf) [__glibcxx_format]: Befriend
__format::_Streambuf_sink.
Co-authored-by: Tomasz Kamiński <tkaminsk@redhat.com>
Signed-off-by: Anlai Lu <agicy@qq.com>
---
v4:
* remove traits parameter, so specialization works only for char_traits,
I do not think that have any realistic impact
* updated friend declarations - the char_traits version is always
befriened, even if traits does not match, given that they are internal
this is OK
* set the _M_write_failed flag if __sbuf is null, and added check
for _M_write_failed in constructor body and _M_reserve
Testing on x86_64-linux wiht next patch. All *format* test already passed
in all standard modes, debug, assertions, cxx98 abi.
OK for trunk when all test passes?
libstdc++-v3/include/std/format | 118 +++++++++++++++++++++++++++++
libstdc++-v3/include/std/streambuf | 9 +++
2 files changed, 127 insertions(+)
Comments
On Fri, 02 Oct 2026 at 14:39 +0200, Tomasz Kamiński wrote:
>From: Anlai Lu <agicy@qq.com>
>
>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 _M_write_failed is set also
>when empty streambuf pointer is provided, as this allow correctly
>propagating failed flag for default constructed ostreambuf_iterator.
>
>The _Streambuf_sink is specialized only for char_traits<_CharT>,
>as only in that case we are guaranteed that __safe_pbump member is
>present.
>
>libstdc++-v3/ChangeLog:
>
> * include/std/format (__format::_Streambuf_sink): New class
> template.
> * include/std/streambuf: Include bits/version.h.
> (__format::_Streambuf_sink) [__glibcxx_format]: Declare.
> (std::basic_streambuf) [__glibcxx_format]: Befriend
> __format::_Streambuf_sink.
>
>Co-authored-by: Tomasz Kamiński <tkaminsk@redhat.com>
>Signed-off-by: Anlai Lu <agicy@qq.com>
>---
>v4:
>* remove traits parameter, so specialization works only for char_traits,
> I do not think that have any realistic impact
>* updated friend declarations - the char_traits version is always
> befriened, even if traits does not match, given that they are internal
> this is OK
>* set the _M_write_failed flag if __sbuf is null, and added check
> for _M_write_failed in constructor body and _M_reserve
>
>Testing on x86_64-linux wiht next patch. All *format* test already passed
>in all standard modes, debug, assertions, cxx98 abi.
>OK for trunk when all test passes?
OK thanks
> libstdc++-v3/include/std/format | 118 +++++++++++++++++++++++++++++
> libstdc++-v3/include/std/streambuf | 9 +++
> 2 files changed, 127 insertions(+)
>
>diff --git a/libstdc++-v3/include/std/format b/libstdc++-v3/include/std/format
>index b4fb59b7397..c7762766909 100644
>--- a/libstdc++-v3/include/std/format
>+++ b/libstdc++-v3/include/std/format
>@@ -3542,6 +3542,124 @@ 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>
>+ class _Streambuf_sink : public _Buf_sink<_CharT>
>+ {
>+ protected:
>+ basic_streambuf<_CharT>* _M_sbuf;
>+ bool _M_write_failed = false;
>+
>+ bool
>+ _M_on_stack() const noexcept
>+ { return this->_M_used().data() == this->_M_buf; }
>+
>+ // Try to point our span directly into the streambuf's put
>+ // area for zero-copy writes. Caller must fall back to
>+ // using local buffer (_M_reset(_M_buf)).
>+ bool
>+ _M_reserve_put_area(size_t __n = 0)
>+ {
>+ if (auto __p = _M_sbuf->pptr())
>+ if (auto __e = _M_sbuf->epptr(); __e > __p)
>+ if (size_t __a(__e - __p); __a >= __n)
>+ {
>+ this->_M_reset(span<_CharT>(__p, __a));
>+ return true;
>+ }
>+ return false;
>+ }
>+
>+ // 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).
>+ void
>+ _M_flush()
>+ {
>+ span<const _CharT> __s = this->_M_used();
>+ if (__s.empty() || _M_write_failed) [[unlikely]]
>+ return;
>+
>+ __try
>+ {
>+ if (!_M_on_stack())
>+ _M_sbuf->__safe_pbump(__s.size());
>+ else if (_M_sbuf->sputn(__s.data(), __s.size())
>+ != static_cast<streamsize>(__s.size()))
>+ _M_write_failed = true;
>+ }
>+ __catch(const __cxxabiv1::__forced_unwind&)
>+ { throw; }
>+ __catch(...)
>+ { _M_write_failed = true; }
>+ }
>+
>+ void
>+ _M_overflow() override
>+ {
>+ _M_flush();
>+ if (!_M_write_failed) [[likely]]
>+ if (_M_reserve_put_area())
>+ return;
>+ this->_M_reset(this->_M_buf);
>+ }
>+
>+ public:
>+ [[__gnu__::__always_inline__]]
>+ explicit
>+ _Streambuf_sink(basic_streambuf<_CharT>* __sbuf) noexcept
>+ : _M_sbuf(__sbuf), _M_write_failed(!__sbuf)
>+ {
>+ if (!_M_write_failed) [[likely]]
>+ _M_reserve_put_area();
>+ }
>+
>+ // _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).
>+
>+ typename _Sink<_CharT>::_Reservation
>+ _M_reserve(size_t __n) override
>+ {
>+ if (_M_write_failed) [[unlikely]]
>+ return { nullptr };
>+
>+ if (__n <= this->_M_unused().size())
>+ return { this };
>+
>+ if (!this->_M_used().empty())
>+ _M_flush();
>+
>+ // Try to write directly into the streambuf's put area.
>+ if (_M_reserve_put_area(__n))
>+ return { this };
>+
>+ // Otherwise reset to the stack buffer.
>+ this->_M_reset(this->_M_buf);
>+ if (__n <= this->_M_unused().size())
>+ return { this };
>+
>+ return { nullptr };
>+ }
>+
>+ bool
>+ _M_discarding() const override
>+ { return _M_write_failed; }
>+
>+ void
>+ _M_finish()
>+ { _M_flush(); }
>+
>+ 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 616e44f74a7..0cc643c2a21 100644
>--- a/libstdc++-v3/include/std/streambuf
>+++ b/libstdc++-v3/include/std/streambuf
>@@ -39,6 +39,8 @@
>
> #include <bits/requires_hosted.h> // iostreams
>
>+#include <bits/version.h>
>+
> #include <bits/c++config.h>
> #include <bits/iosfwd.h>
> #include <bits/localefwd.h>
>@@ -57,6 +59,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> __copy_streambufs_eof(basic_streambuf<_CharT, _Traits>*,
> basic_streambuf<_CharT, _Traits>*, bool&);
>
>+#ifdef __glibcxx_format // C++ >= 20 && HOSTED
>+ namespace __format { template<typename> class _Streambuf_sink; }
>+#endif
>+
> /**
> * @brief The actual work of input and output (interface).
> * @ingroup io
>@@ -149,6 +155,9 @@ _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>;
>+#ifdef __glibcxx_format // C++ >= 20 && HOSTED
>+ friend class __format::_Streambuf_sink<char_type>;
>+#endif
>
> friend streamsize
> __copy_streambufs_eof<>(basic_streambuf*, basic_streambuf*, bool&);
>--
>2.55.0
>
>
@@ -3542,6 +3542,124 @@ 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>
+ class _Streambuf_sink : public _Buf_sink<_CharT>
+ {
+ protected:
+ basic_streambuf<_CharT>* _M_sbuf;
+ bool _M_write_failed = false;
+
+ bool
+ _M_on_stack() const noexcept
+ { return this->_M_used().data() == this->_M_buf; }
+
+ // Try to point our span directly into the streambuf's put
+ // area for zero-copy writes. Caller must fall back to
+ // using local buffer (_M_reset(_M_buf)).
+ bool
+ _M_reserve_put_area(size_t __n = 0)
+ {
+ if (auto __p = _M_sbuf->pptr())
+ if (auto __e = _M_sbuf->epptr(); __e > __p)
+ if (size_t __a(__e - __p); __a >= __n)
+ {
+ this->_M_reset(span<_CharT>(__p, __a));
+ return true;
+ }
+ return false;
+ }
+
+ // 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).
+ void
+ _M_flush()
+ {
+ span<const _CharT> __s = this->_M_used();
+ if (__s.empty() || _M_write_failed) [[unlikely]]
+ return;
+
+ __try
+ {
+ if (!_M_on_stack())
+ _M_sbuf->__safe_pbump(__s.size());
+ else if (_M_sbuf->sputn(__s.data(), __s.size())
+ != static_cast<streamsize>(__s.size()))
+ _M_write_failed = true;
+ }
+ __catch(const __cxxabiv1::__forced_unwind&)
+ { throw; }
+ __catch(...)
+ { _M_write_failed = true; }
+ }
+
+ void
+ _M_overflow() override
+ {
+ _M_flush();
+ if (!_M_write_failed) [[likely]]
+ if (_M_reserve_put_area())
+ return;
+ this->_M_reset(this->_M_buf);
+ }
+
+ public:
+ [[__gnu__::__always_inline__]]
+ explicit
+ _Streambuf_sink(basic_streambuf<_CharT>* __sbuf) noexcept
+ : _M_sbuf(__sbuf), _M_write_failed(!__sbuf)
+ {
+ if (!_M_write_failed) [[likely]]
+ _M_reserve_put_area();
+ }
+
+ // _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).
+
+ typename _Sink<_CharT>::_Reservation
+ _M_reserve(size_t __n) override
+ {
+ if (_M_write_failed) [[unlikely]]
+ return { nullptr };
+
+ if (__n <= this->_M_unused().size())
+ return { this };
+
+ if (!this->_M_used().empty())
+ _M_flush();
+
+ // Try to write directly into the streambuf's put area.
+ if (_M_reserve_put_area(__n))
+ return { this };
+
+ // Otherwise reset to the stack buffer.
+ this->_M_reset(this->_M_buf);
+ if (__n <= this->_M_unused().size())
+ return { this };
+
+ return { nullptr };
+ }
+
+ bool
+ _M_discarding() const override
+ { return _M_write_failed; }
+
+ void
+ _M_finish()
+ { _M_flush(); }
+
+ 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).
@@ -39,6 +39,8 @@
#include <bits/requires_hosted.h> // iostreams
+#include <bits/version.h>
+
#include <bits/c++config.h>
#include <bits/iosfwd.h>
#include <bits/localefwd.h>
@@ -57,6 +59,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
__copy_streambufs_eof(basic_streambuf<_CharT, _Traits>*,
basic_streambuf<_CharT, _Traits>*, bool&);
+#ifdef __glibcxx_format // C++ >= 20 && HOSTED
+ namespace __format { template<typename> class _Streambuf_sink; }
+#endif
+
/**
* @brief The actual work of input and output (interface).
* @ingroup io
@@ -149,6 +155,9 @@ _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>;
+#ifdef __glibcxx_format // C++ >= 20 && HOSTED
+ friend class __format::_Streambuf_sink<char_type>;
+#endif
friend streamsize
__copy_streambufs_eof<>(basic_streambuf*, basic_streambuf*, bool&);