On Thu, 4 Sept 2025 at 14:39, Tomasz Kaminski <[email protected]> wrote:
>
>
>
> On Thu, Sep 4, 2025 at 2:53 PM Tomasz Kamiński <[email protected]> wrote:
>>
>> When producing output, the libstdc++ format implementation only uses
>> _Sink_iter
>> specializations. Since users cannot construct basic_format_context, this is
>> the
>> only iterator type actually used. The __format_padded helper relies on this
>> property to efficiently pad sequences from tuples and ranges.
>>
>> However, the standard's formattable concept requires a generic format
>> function
>> in formatters that works with an any iterator type. This is intended to
>> future-proof the implementation by allowing new format_context types.
>> Previously,
>> libstdc++ used back_insert_iterator<basic_string<_CharT>> for this purpose.
>>
>> Normally, concept checks only instantiate function signatures, but with
>> user-defined formatters and deduced return types, the function body and all
>> called functions are instantiated. This could trigger a static assertion
>> error
>> in the range/tuple formatter that assumed the iterator was a _Sink_iter
>> (see included test).
>>
>> This patch resolves the issue by replacing the _Iter_for_t alias with the
>> internal _Drop_iter. This iterator sematnics is to drop elements, so
>> __format_padded can handle it by simply returning the input iterator, which
>> still produces the required behavior [1].
>>
>> An alternative of using _Sink_iter was considered but rejected because it
>> would
>> allow formatters to pass formattable requirements while only supporting
>> format_context and wformat_context, which seems counter to the design intent
>> (the std/format/formatter/concept.cc fails).
>>
>> [1] The standard's wording defines format functions as producing an output
>> representation, but does not explicitly require a formatter to be invoked
>> for each element. This allows the use of _Drop_iter to pass the concept check
>> without generating any output.
>>
>> PR libstdc++/121765
>>
>> libstdc++-v3/ChangeLog:
>>
>> * include/std/format (__format::_Drop_iter): Define.
>> (_Iter_for_t::type): Change alias to _Drop_iter.
>> (__format::__format_padded): Return __fc.out() for
>> _Drop_iter.
>> * testsuite/std/format/pr121765.cc: New test.
>> ---
>> The description of this patch is longer that then change, but I think
>> to record why I have choosen particular direction.
>>
>> Tested on x86_64-linux locally. OK for trunk?
>>
>> libstdc++-v3/include/std/format | 120 ++++++++++++------
>> libstdc++-v3/testsuite/std/format/pr121765.cc | 53 ++++++++
>> 2 files changed, 136 insertions(+), 37 deletions(-)
>> create mode 100644 libstdc++-v3/testsuite/std/format/pr121765.cc
>>
>> diff --git a/libstdc++-v3/include/std/format
>> b/libstdc++-v3/include/std/format
>> index d63c6fc9efd..e6377aaa2ed 100644
>> --- a/libstdc++-v3/include/std/format
>> +++ b/libstdc++-v3/include/std/format
>> @@ -56,7 +56,7 @@
>> #include <bits/ranges_base.h> // input_range, range_reference_t
>> #include <bits/ranges_util.h> // subrange
>> #include <bits/ranges_algobase.h> // ranges::copy
>> -#include <bits/stl_iterator.h> // back_insert_iterator, counted_iterator
>> +#include <bits/stl_iterator.h> // counted_iterator
>> #include <bits/stl_pair.h> // __is_pair
>> #include <bits/unicode.h> // __is_scalar_value, _Utf_view, etc.
>> #include <bits/utility.h> // tuple_size_v
>> @@ -110,10 +110,14 @@ namespace __format
>> template<typename _CharT>
>> class _Sink_iter;
>>
>> + // Output iterator that ignores the characters
>> + template<typename _CharT>
>> + class _Drop_iter;
>> +
>> // An unspecified output iterator type used in the `formattable` concept.
>> template<typename _CharT>
>> struct _Iter_for
>> - { using type = back_insert_iterator<basic_string<_CharT>>; };
>> + { using type = _Drop_iter<_CharT>; };
>>
>> template<typename _CharT>
>> using __format_context = basic_format_context<_Sink_iter<_CharT>,
>> _CharT>;
>> @@ -3135,6 +3139,43 @@ _GLIBCXX_END_NAMESPACE_CONTAINER
>> /// @cond undocumented
>> namespace __format
>> {
>> + template<typename _CharT>
>> + class _Drop_iter
>> + {
>> + public:
>> + using iterator_category = output_iterator_tag;
>> + using value_type = void;
>> + using difference_type = ptrdiff_t;
>> + using pointer = void;
>> + using reference = void;
>> +
>> + _Drop_iter() = default;
>> + _Drop_iter(const _Drop_iter&) = default;
>> + _Drop_iter& operator=(const _Drop_iter&) = default;
>> +
>> + [[__gnu__::__always_inline__]]
>> + constexpr _Drop_iter&
>> + operator=(_CharT __c)
>> + { return *this; }
>> +
>> + [[__gnu__::__always_inline__]]
>> + constexpr _Drop_iter&
>> + operator=(basic_string_view<_CharT> __s)
>> + { return *this; }
>> +
>> + [[__gnu__::__always_inline__]]
>> + constexpr _Drop_iter&
>> + operator*() { return *this; }
>> +
>> + [[__gnu__::__always_inline__]]
>> + constexpr _Drop_iter&
>> + operator++() { return *this; }
>> +
>> + [[__gnu__::__always_inline__]]
>> + constexpr _Drop_iter
>> + operator++(int) { return *this; }
>> + };
>> +
>> template<typename _CharT>
>> class _Sink_iter
>> {
>> @@ -5503,42 +5544,47 @@ namespace __format
>> const _Spec<_CharT>& __spec,
>> _Callback&& __call)
>> {
>> - // This is required to implement formatting with padding,
>> - // as we need to format to temporary buffer, using the same iterator.
>> - static_assert(is_same_v<_Out, __format::_Sink_iter<_CharT>>);
>> -
>> - const size_t __padwidth = __spec._M_get_width(__fc);
>> - if (__padwidth == 0)
>> - return __call(__fc);
>> -
>> - struct _Restore_out
>> - {
>> - _Restore_out(basic_format_context<_Sink_iter<_CharT>, _CharT>& __fc)
>> - : _M_ctx(std::addressof(__fc)), _M_out(__fc.out())
>> - { }
>> -
>> - void
>> - _M_disarm()
>> - { _M_ctx = nullptr; }
>> -
>> - ~_Restore_out()
>> - {
>> - if (_M_ctx)
>> - _M_ctx->advance_to(_M_out);
>> - }
>> -
>> - private:
>> - basic_format_context<_Sink_iter<_CharT>, _CharT>* _M_ctx;
>> - _Sink_iter<_CharT> _M_out;
>> - };
>> + if constexpr (is_same_v<_Out, _Iter_for_t<_CharT>>)
>
> This would be better if we use _Drop_iter here. This is result of me doing
> some other tests.
Yes, I agree it should be _Drop_iter<_CharT>. That way if we ever
change the _Iter_for<C>::type to be _Iter_sink<C> this won't break, or
if we change it to some different type then we will have to explicitly
consider how to handle that type here (it's safe to ignore _Drop_iter,
but might not be for some other type).
Alternatively, we could just remove _Iter_for and define
_Iter_for_t<C> as type_identity_t<_Drop_iter<C>>, or just
_Drop_iter<C> directly?
>>
>> + return __fc.out();
>> + else
>> + {
>> + // This is required to implement formatting with padding,
>> + // as we need to format to temporary buffer, using the same
>> iterator.
>> + static_assert(is_same_v<_Out, _Sink_iter<_CharT>>);
>> +
>> + const size_t __padwidth = __spec._M_get_width(__fc);
>> + if (__padwidth == 0)
>> + return __call(__fc);
>> +
>> + struct _Restore_out
>> + {
>> + _Restore_out(basic_format_context<_Sink_iter<_CharT>, _CharT>&
>> __fc)
>> + : _M_ctx(std::addressof(__fc)), _M_out(__fc.out())
>> + { }
>> +
>> + void
>> + _M_disarm()
>> + { _M_ctx = nullptr; }
>> +
>> + ~_Restore_out()
>> + {
>> + if (_M_ctx)
>> + _M_ctx->advance_to(_M_out);
>> + }
>>
>> - _Restore_out __restore(__fc);
>> - _Padding_sink<_Sink_iter<_CharT>, _CharT> __sink(__fc.out(),
>> __padwidth);
>> - __fc.advance_to(__sink.out());
>> - __call(__fc);
>> - __fc.advance_to(__sink._M_finish(__spec._M_align, __spec._M_fill));
>> - __restore._M_disarm();
>> - return __fc.out();
>> + private:
>> + basic_format_context<_Sink_iter<_CharT>, _CharT>* _M_ctx;
>> + _Sink_iter<_CharT> _M_out;
>> + };
>> +
>> + _Restore_out __restore(__fc);
>> + _Padding_sink<_Sink_iter<_CharT>, _CharT> __sink(__fc.out(),
>> __padwidth);
>> + __fc.advance_to(__sink.out());
>> + __call(__fc);
>> + __fc.advance_to(__sink._M_finish(__spec._M_align, __spec._M_fill));
>> + __restore._M_disarm();
>> + return __fc.out();
>> + }
>> }
>>
>> template<size_t _Pos, typename _Tp, typename _CharT>
>> diff --git a/libstdc++-v3/testsuite/std/format/pr121765.cc
>> b/libstdc++-v3/testsuite/std/format/pr121765.cc
>> new file mode 100644
>> index 00000000000..1358fc11052
>> --- /dev/null
>> +++ b/libstdc++-v3/testsuite/std/format/pr121765.cc
>> @@ -0,0 +1,53 @@
>> +// { dg-do compile { target c++23 } }
>> +
>> +#include <format>
>> +#include <utility>
>> +
>> +struct MyPair
>> +{
>> + int x;
>> + int y;
>> +};
>> +
>> +template<typename CharT>
>> +struct std::formatter<MyPair, CharT>
>> +{
>> + template<typename ParseContext>
>> + auto parse(ParseContext& pc)
>> + { return _formatter.parse(pc); }
>> +
>> + template<typename FormatContext>
>> + auto format(const MyPair& mp, FormatContext& fc) const
>> + { return _formatter.format(std::make_pair(mp.x, mp.y), fc); }
>> +
>> +private:
>> + std::formatter<std::pair<int, int>, CharT> _formatter;
>> +};
>> +
>> +static_assert(std::formattable<MyPair, char>);
>> +static_assert(std::formattable<MyPair, wchar_t>);
>> +
>> +struct MyRange
>> +{
>> + int* begin;
>> + int* end;
>> +};
>> +
>> +template<typename CharT>
>> +struct std::formatter<MyRange, CharT>
>> +{
>> + template<typename ParseContext>
>> + auto parse(ParseContext& pc)
>> + { return _formatter.parse(pc); }
>> +
>> + template<typename FormatContext>
>> + auto format(const MyRange& mp, FormatContext& fc) const
>> + { return _formatter.format(std::span<int>(mp.begin, mp.end), fc); }
>> +
>> +private:
>> + std::formatter<std::span<int>, CharT> _formatter;
>> +};
>> +
>> +static_assert(std::formattable<MyRange, char>);
>> +static_assert(std::formattable<MyRange, wchar_t>);
>> +
>> --
>> 2.51.0
>>