[v4,2/4] libstdc++: Add _Streambuf_sink for direct streambuf formatting

Message ID 20261002124906.1093441-1-tkaminsk@redhat.com
State Committed
Commit d9b799e27aa3cb7d93ccaa7c6a64663f4c457deb
Headers
Series None |

Commit Message

Tomasz Kaminski Oct. 2, 2026, 12:39 p.m. UTC
  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

Jonathan Wakely Oct. 2, 2026, 12:54 p.m. UTC | #1
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
>
>
  

Patch

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&);