On Wed, 9 Sep 2026, Jonathan Wakely wrote:
> This allows the integer and string forms to work with Clang, as they no
> longer need to instantiate an out-of-line function template. The generic
> check_dynamic_spec<T...>() form still won't work with Clang until they
> fix their bug, but most users will never notice that.
I'm happy to inform you that your patch is better than you thought: the
generic form does work with Clang, now that r17-4030 is in.
I had Claude test a user formatter that calls check_dynamic_spec<int,
unsigned, long long, unsigned long long, bool, C,
basic_string_view<C>>(id) from parse(), for char and wchar_t, against
trunk with your new patch applied. It found that clang 21.1.0, 22.1.0,
23.1 and trunk all accept "{:{}}" with an int, unsigned or bool width
at -std=c++26, and all reject a double width with the "arg(id) type
does not match" diagnostic, so the check really runs.
That makes sense: the Clang bug only bites when a template is first
used before its definition, and a user formatter is parsed after
<format> is complete. What kept the generic form from working with
Clang until r17-4030 landed was the static_assert on the non-static
__valid_types_for_check_dynamic_spec, which that commit fixed.
So, if you like, the caveat can be removed. Between r17-4030 and this
patch, Clang 21 and later accept everything in the check_dynamic_spec
family, and nothing remains to track on the libstdc++ side. Test case
below.
Thanks again for taking care of this.
Steve
#include <format>
#include <string_view>
struct W { int v; };
template<typename C> struct std::formatter<W, C> {
constexpr auto parse(std::basic_format_parse_context<C>& pc) {
auto it = pc.begin();
if (it != pc.end() && *it == '{') {
++it;
pc.template check_dynamic_spec<int, unsigned, long long,
unsigned long long, bool, C, std::basic_string_view<C>>(
pc.next_arg_id());
++it;
}
return it;
}
template<typename Out>
auto format(W, std::basic_format_context<Out, C>& fc) const {
return std::format_to(fc.out(), std::basic_string_view<C>{});
}
};
int main() {
auto a = std::format("{:{}}", W{1}, 2); // int width, char
auto b = std::format(L"{:{}}", W{1}, 2u); // unsigned width, wchar_t
auto c = std::format("{:{}}", W{1}, true); // bool is in the pack
#ifdef NEG
auto d = std::format("{:{}}", W{1}, 3.5); // double not in pack
#endif
return a.size() + b.size() + c.size() ? 0 : 1;
}
On Wed, Sep 9, 2026 at 4:27 PM Jonathan Wakely <[email protected]> wrote:
>
> Clang cannot compile format_parse_context::check_dynamic_spec due to a
> longstanding bug[1] with out-of-line function templates called during
> constant evaluation. This patch refactors check_dynamic_spec and related
> functions, so that the definitions are inline in the class body.
>
> To make this work we move the definition of __format::_Arg_t earlier,
> and redefine _Scanner<C>::_Parse_context as a nested class within
> basic_format_parse_context<C>, called _Scan_parse_context. The
> __check_dynamic_spec function is changed to take a span<_Arg_t> instead
> of being instantiated with a template parameter pack of arg types. The
> callers that have fixed arg types can pass in an array of the fixed
> integer or string types, and the generic form (which is rarely needed in
> user code) does the conversion using the __to_arg_t_enum function.
>
> This allows the integer and string forms to work with Clang, as they no
> longer need to instantiate an out-of-line function template. The generic
> check_dynamic_spec<T...>() form still won't work with Clang until they
> fix their bug, but most users will never notice that.
>
> [1]: https://github.com/llvm/llvm-project/issues/73232
>
> libstdc++-v3/ChangeLog:
>
> PR libstdc++/127220
> * include/std/format (__format::_Arg_t): Move definition
> earlier.
> (basic_format_parse_context::check_dynamic_spec): Construct
> array of _Arg_t from template parameter list.
> (basic_format_parse_context::check_dynamic_spec_integral):
> Construct array of _Arg_t based on supported types.
> (basic_format_parse_context::check_dynamic_spec_string):
> Likewise.
> (basic_format_parse_context::__check_dynamic_spec): Replace
> template parameter list with span<const _Arg_t> function
> parameter and simplify accordingly. Define in class body.
> (basic_format_parse_context::_Scan_parse_context): New struct
> the replaces _Scanner::_Parse_context.
> (__format::_Scanner::_M_pc): Change type to _Scan_parse_context.
> ---
>
> Tested x86_64-linux, and manually confirmed that the testcase in the PR
> works with Clang 21.1.8
>
> libstdc++-v3/include/std/format | 126 ++++++++++++++++----------------
> 1 file changed, 63 insertions(+), 63 deletions(-)
>
> diff --git a/libstdc++-v3/include/std/format b/libstdc++-v3/include/std/format
> index 730c309a97d3..90576f022705 100644
> --- a/libstdc++-v3/include/std/format
> +++ b/libstdc++-v3/include/std/format
> @@ -275,6 +275,24 @@ namespace __format
>
> template<typename _CharT> class _Scanner;
>
> + enum class _Arg_t : unsigned char {
> + _Arg_none, _Arg_bool, _Arg_c, _Arg_i, _Arg_u, _Arg_ll, _Arg_ull,
> + _Arg_flt, _Arg_dbl, _Arg_ldbl, _Arg_str, _Arg_sv, _Arg_ptr, _Arg_handle,
> + _Arg_i128, _Arg_u128, _Arg_float128,
> + _Arg_bf16, _Arg_f16, _Arg_f32, _Arg_f64,
> + _Arg_max_,
> +
> +#ifdef _GLIBCXX_LONG_DOUBLE_ALT128_COMPAT
> + _Arg_ibm128 = _Arg_ldbl,
> + _Arg_ieee128 = _Arg_float128,
> +#endif
> + };
> + using enum _Arg_t;
> +
> + template<typename _CharT, typename _Tp>
> + consteval _Arg_t
> + __to_arg_t_enum() noexcept;
> +
> } // namespace __format
> /// @endcond
>
> @@ -337,7 +355,7 @@ namespace __format
> __format::__invalid_arg_id_in_format_string();
> }
>
> -#if __cpp_lib_format >= 202305L
> +#if __cpp_lib_format >= 202305L // >= C++26
> template<typename... _Ts>
> constexpr void
> check_dynamic_spec(size_t __id) noexcept
> @@ -345,17 +363,22 @@ namespace __format
> static_assert(__valid_types_for_check_dynamic_spec<_Ts...>(),
> "template arguments for check_dynamic_spec<Ts...>(id)
> "
> "must be unique and must be one of the allowed
> types");
> - if consteval {
> - __check_dynamic_spec<_Ts...>(__id);
> - }
> + if constexpr (sizeof...(_Ts))
> + if consteval {
> + const __format::_Arg_t __t[] = {
> + __format::__to_arg_t_enum<_CharT, _Ts>()...
> + };
> + __check_dynamic_spec(__id, __t);
> + }
> }
>
> constexpr void
> check_dynamic_spec_integral(size_t __id) noexcept
> {
> if consteval {
> - __check_dynamic_spec<int, unsigned, long long,
> - unsigned long long>(__id);
> + using enum __format::_Arg_t;
> + const __format::_Arg_t __t[] = { _Arg_i, _Arg_u, _Arg_ll, _Arg_ull
> };
> + __check_dynamic_spec(__id, __t);
> }
> }
>
> @@ -363,7 +386,9 @@ namespace __format
> check_dynamic_spec_string(size_t __id) noexcept
> {
> if consteval {
> - __check_dynamic_spec<const _CharT*,
> basic_string_view<_CharT>>(__id);
> + using enum __format::_Arg_t;
> + const __format::_Arg_t __t[] = { _Arg_str, _Arg_sv };
> + __check_dynamic_spec(__id, __t);
> }
> }
>
> @@ -403,14 +428,27 @@ namespace __format
> }
> }
>
> - template<typename... _Ts>
> - consteval void
> - __check_dynamic_spec(size_t __id) noexcept;
> + // Common implementation of check_dynamic_spec{,_string,_integral}
> + consteval void
> + __check_dynamic_spec(size_t __id,
> + span<const __format::_Arg_t> __types) noexcept
> + {
> + if (__id >= _M_num_args)
> + __format::__invalid_arg_id_in_format_string();
> +
> + if (auto* __args = static_cast<_Scan_parse_context*>(this)->_M_types)
> + {
> + for (auto __t : __types)
> + if (__args[__id] == __t)
> + return;
> +
> + __invalid_dynamic_spec("arg(id) type does not match");
> + }
> + // else this is a formatting scanner, do not do any type checks.
> + }
>
> // This must not be constexpr.
> static void __invalid_dynamic_spec(const char*);
> -
> - friend __format::_Scanner<_CharT>;
> #endif
>
> // This constructor should only be used by the implementation.
> @@ -427,6 +465,18 @@ namespace __format
> _Indexing _M_indexing = _Unknown;
> size_t _M_next_arg_id = 0;
> size_t _M_num_args = 0;
> +
> + // Derived parse context used by Scanner when checking format strings.
> + struct _Scan_parse_context;
> + friend __format::_Scanner<_CharT>;
> + };
> +
> + template<typename _CharT>
> + struct basic_format_parse_context<_CharT>::_Scan_parse_context
> + : basic_format_parse_context<_CharT>
> + {
> + using basic_format_parse_context<_CharT>::basic_format_parse_context;
> + const __format::_Arg_t* _M_types = nullptr;
> };
>
> /// @cond undocumented
> @@ -4107,20 +4157,6 @@ namespace __format
> }
> };
>
> - enum class _Arg_t : unsigned char {
> - _Arg_none, _Arg_bool, _Arg_c, _Arg_i, _Arg_u, _Arg_ll, _Arg_ull,
> - _Arg_flt, _Arg_dbl, _Arg_ldbl, _Arg_str, _Arg_sv, _Arg_ptr, _Arg_handle,
> - _Arg_i128, _Arg_u128, _Arg_float128,
> - _Arg_bf16, _Arg_f16, _Arg_f32, _Arg_f64,
> - _Arg_max_,
> -
> -#ifdef _GLIBCXX_LONG_DOUBLE_ALT128_COMPAT
> - _Arg_ibm128 = _Arg_ldbl,
> - _Arg_ieee128 = _Arg_float128,
> -#endif
> - };
> - using enum _Arg_t;
> -
> template<typename _Context>
> struct _Arg_value
> {
> @@ -5066,11 +5102,7 @@ namespace __format
> {
> using iterator = typename basic_format_parse_context<_CharT>::iterator;
>
> - struct _Parse_context : basic_format_parse_context<_CharT>
> - {
> - using basic_format_parse_context<_CharT>::basic_format_parse_context;
> - const _Arg_t* _M_types = nullptr;
> - } _M_pc;
> + typename basic_format_parse_context<_CharT>::_Scan_parse_context _M_pc;
>
> constexpr explicit
> _Scanner(basic_string_view<_CharT> __str, size_t __nargs = (size_t)-1)
> @@ -5437,38 +5469,6 @@ namespace __format
> } // namespace __format
> /// @endcond
>
> -#if __cpp_lib_format >= 202305L // >= C++26
> - /// @cond undocumented
> - // Common implementation of check_dynamic_spec{,_string,_integral}
> - template<typename _CharT>
> - template<typename... _Ts>
> - consteval void
> - basic_format_parse_context<_CharT>::
> - __check_dynamic_spec(size_t __id) noexcept
> - {
> - if (__id >= _M_num_args)
> - __format::__invalid_arg_id_in_format_string();
> - if constexpr (sizeof...(_Ts) != 0)
> - {
> - using _Parse_ctx = __format::_Scanner<_CharT>::_Parse_context;
> - auto* __args = static_cast<_Parse_ctx*>(this)->_M_types;
> - // Formatting scanner, no type check.
> - if (!__args)
> - return;
> -
> - auto __arg = __args[__id];
> - __format::_Arg_t __types[] = {
> - __format::__to_arg_t_enum<_CharT, _Ts>()...
> - };
> - for (auto __t : __types)
> - if (__arg == __t)
> - return;
> - }
> - __invalid_dynamic_spec("arg(id) type does not match");
> - }
> - /// @endcond
> -#endif
> -
> template<typename _CharT, typename... _Args>
> template<typename _Tp>
> requires convertible_to<const _Tp&, basic_string_view<_CharT>>
> --
> 2.55.0
>