Re: [PATCH v2 3/4] libstdc++: Specialize _Iter_sink for ostreambuf_iterator
Anlai Lu <[email protected]>
| Newsgroups | gmane.comp.gcc.patches,gmane.comp.gcc.libstdc++.devel |
|---|---|
| Message-ID | <[email protected]> |
> On Aug 18, 2026, at 14:18, Anlai Lu <[email protected]> wrote: > > Hi Tomasz, > > Thanks for the patch. I have reviewed it previously but just now I ran > some additional tests. During testing, I found two issues that should > be fixed before this is merged. > >> On Aug 11, 2026, at 00:11, Tomasz Kamiński <[email protected]> wrote: >> >> From: Anlai Lu <[email protected]> >> >> Add partial specialization of _Iter_sink for ostreambuf_iterator >> that inherits _Streambuf_sink, replacing per-character sputc with >> bulk sputn and zero-copy put-area writes. >> >> All counting and truncation (_M_max) is handled in this >> specialization so that _Streambuf_sink stays a pure I/O layer. >> _M_overflow counts all characters and only writes up to the limit, >> so format_to_n can compute the total output length. _M_discarding >> returns false for the same reason. Stack writes go through >> _M_out._M_put() which tracks failure on the iterator. sputn >> exceptions are caught and the iterator is marked failed via >> _M_set_failed(). >> >> The maximum count uses size_t with _S_no_limit sentinel, matching >> the _Ptr_sink convention and avoiding signed comparisons. >> >> libstdc++-v3/ChangeLog: >> >> * include/bits/streambuf_iterator.h (__format::_Iter_sink) >> [__glibcxx_format]: Declare with specialization for >> ostreambuf_iterator. >> (std::ostreambuf_iterator) [__glibcxx_format]: Befriend >> __format::_Iter_sink for ostreambuf_iterator. >> * include/std/format (_Iter_sink<_CharT, ostreambuf_iterator<...>>): >> Define new partial specialization. >> >> Co-authored-by: Tomasz Kamiński <[email protected]> >> Signed-off-by: Anlai Lu <[email protected]> >> Signed-off-by: Tomasz Kamiński <[email protected]> >> --- >> Changes in v2: >> - limit unused/reserved buffer to the remaining characters >> in _M_overflow/_M_reserve and constructor. Mostly via >> calling _M_trim_buf. Rewrote _M_overflow in process >> - overloads constructor without limit to avoid above >> - befriend _Iter_sink specialization with ostreambuf_iterator >> - remove _M_out member and recosntructor iterator in _M_finish >> >> Testing on powerprc64. Tested on x86_64, additionally tested >> *format* and *print* in all standard modes, assertions and >> debug. OK for trunk? >> >> .../include/bits/streambuf_iterator.h | 12 +++ >> libstdc++-v3/include/std/format | 82 +++++++++++++++++++ >> 2 files changed, 94 insertions(+) >> >> diff --git a/libstdc++-v3/include/bits/streambuf_iterator.h b/libstdc++-v3/include/bits/streambuf_iterator.h >> index 095928ca4d8..d715edc24ae 100644 >> --- a/libstdc++-v3/include/bits/streambuf_iterator.h >> +++ b/libstdc++-v3/include/bits/streambuf_iterator.h >> @@ -42,6 +42,14 @@ namespace std _GLIBCXX_VISIBILITY(default) >> { >> _GLIBCXX_BEGIN_NAMESPACE_VERSION >> >> +#ifdef __glibcxx_format // C++ >= 20 && HOSTED >> + namespace __format { >> + template<typename, typename> class _Iter_sink; >> + template<typename _CharT, typename _Traits> >> + class _Iter_sink<_CharT, ostreambuf_iterator<_CharT, _Traits>>; >> + } >> +#endif >> + >> /** >> * @addtogroup iterators >> * @{ >> @@ -266,6 +274,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION >> copy(istreambuf_iterator<_CharT2>, istreambuf_iterator<_CharT2>, >> ostreambuf_iterator<_CharT2>); >> >> +#ifdef __glibcxx_format // C++ >= 20 && HOSTED >> + friend class __format::_Iter_sink<char_type, ostreambuf_iterator>; >> +#endif >> + >> private: >> streambuf_type* _M_sbuf; >> bool _M_failed; >> diff --git a/libstdc++-v3/include/std/format b/libstdc++-v3/include/std/format >> index 9223cd2e78b..ad6a56ecc3a 100644 >> --- a/libstdc++-v3/include/std/format >> +++ b/libstdc++-v3/include/std/format >> @@ -3847,6 +3847,88 @@ namespace __format >> } >> }; >> >> + // Specialization replacing per-character sputc with bulk sputn >> + // and zero-copy writes into the streambuf's put area. >> + template<typename _CharT, typename _Traits> >> + class _Iter_sink<_CharT, ostreambuf_iterator<_CharT, _Traits>> >> + : public _Streambuf_sink<_CharT, _Traits> >> + { >> + using _Base = _Streambuf_sink<_CharT, _Traits>; >> + using _OutIter = ostreambuf_iterator<_CharT, _Traits>; >> + >> + static constexpr size_t _S_no_limit = size_t(-1); >> + >> + void >> + _M_trim_buf() >> + { >> + const size_t __avail = _M_max - _M_count; >> + if (this->_M_unused().size() > __avail) >> + this->_M_reset(this->_M_unused().first(__avail)); >> + } >> + >> + protected: >> + size_t _M_max = _S_no_limit; >> + size_t _M_count = 0; >> + >> + void >> + _M_overflow() override >> + { >> + const size_t __new = this->_M_used().size(); >> + if (_M_count >= _M_max) >> + this->_M_reset(this->_M_buf); >> + else if (size_t __a = _M_max - _M_count; __a > __new) >> + { >> + _Base::_M_overflow(); >> + _M_trim_buf(); > > _M_trim_buf() depends on the value of _M_count. In this else-if > branch it is called before _M_count is updated, which leads to an > incorrect trim. > >> + } >> + else >> + { >> + this->_M_flush(); >> + this->_M_reset(this->_M_buf); >> + } >> + _M_count += __new; >> + } >> + >> + bool >> + _M_discarding() const override >> + { return false; } >> + >> + public: >> + [[__gnu__::__always_inline__]] >> + explicit >> + _Iter_sink(_OutIter __out) >> + : _Base(__out._M_sbuf) >> + { } >> + >> + [[__gnu__::__always_inline__]] >> + explicit >> + _Iter_sink(_OutIter __out, iter_difference_t<_OutIter> __max) >> + : _Base(__out._M_sbuf), _M_max(__max < 0 ? _S_no_limit : size_t(__max)) >> + { _M_trim_buf(); } >> + >> + typename _Sink<_CharT>::_Reservation >> + _M_reserve(size_t __n) override >> + { >> + if (_M_count < _M_max) >> + if (size_t __a = _M_max - _M_count; __a >= __n) >> + return _Base::_M_reserve(__n); >> + return { nullptr }; >> + } >> + >> + format_to_n_result<_OutIter> >> + _M_finish() && >> + { >> + _M_count += this->_M_used().size(); >> + if (_M_count < _M_max) >> + this->_M_flush(); > > This condition should be _M_count <= _M_max. > When _M_count becomes exactly _M_max, there may still be data > that has not been flushed. > >> + >> + _OutIter __out; >> + __out._M_sbuf = this->_M_sbuf; >> + __out._M_failed = this->_M_write_failed; >> + return { std::move(__out), ptrdiff_t(_M_count) }; >> + } >> + }; >> + >> // Used for contiguous iterators. >> // No buffer is used, characters are written straight to the iterator. >> // We do not know the size of the output range, so the span size just grows >> -- >> 2.55.0 > > Two issues above can be covered by this test case: > > std::string big(257, 'A'); > std::ostringstream os; > auto res = std::format_to_n(std::ostreambuf_iterator<char>(os), > 257, "{}", big); > VERIFY(res.size == 257); > VERIFY(os.view() == big); > > The existing test suite does not cover this exact-boundary case, which > is why these issues were not caught earlier. The existing cases in > test_format_to_n have a (len=1000, limit=256) and (len=8, limit=8), but > does not cover the case (len > 256 && len = limit). Quick follow up: I previously wrote that the (len=8, limit=8) test passes because 8 < 256. That is not the full picture. The real reason is that the len=limit=8 case uses {}{}, "AAAA", "BBBB", which has intermediate flush. The second issue can be found with just std::format_to_n(oit, 1, "{}", "A"); Sorry for the confusion in my earlier analysis.