https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127220

            Bug ID: 127220
           Summary: <format>: clang rejects every dynamic width or
                    precision ("{:{}}") in C++26 mode:
                    __check_dynamic_spec is instantiated before its
                    definition
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: stevensudit at gmail dot com
  Target Milestone: ---

Created attachment 65510
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65510&action=edit
test: format strings with a dynamic width or precision

FILE SECOND. Bugzilla fields as for report 1: Product gcc, Component
libstdc++, Version 16.2.0, x86_64-linux-gnu, Known to fail 15.2.0, 16.2.0,
17.0, no keywords, normal, P3.
  Summary: <format>: clang rejects every dynamic width or precision
           ("{:{}}") in C++26 mode: __check_dynamic_spec is instantiated
           before its definition
  Attachments (text/plain): format_dynamic_width.cc,
           reduce_a_late_definition.cc,
           libstdcxx-format-late-definition.diff (patch)
  Line numbers below are for the 16.2.0 release header.

Description

With clang on top of libstdc++ in -std=c++26 mode, every format string
that has a dynamic width or precision is rejected at compile time:

  #include <format>
  int main() { (void)std::format("{:{}}", 42, 6); }

  $ clang++ -std=c++26 -fsyntax-only t.cc
  t.cc:2:32: error: call to consteval function
    'std::basic_format_string<char, int, int>::basic_format_string<char[6]>'
    is not a constant expression
  format:355:4: note: undefined function '__check_dynamic_spec<int,
    unsigned int, long long, unsigned long long>' cannot be used in a
    constant expression

The same happens for "{:.{}f}" and L"{:{}}" (attached
format_dynamic_width.cc). -std=c++23 is fine, since the check_dynamic_spec
code is only compiled for __cpp_lib_format >= 202305L, and g++ 16.2 and
trunk accept the file in every mode, as does EDG 6.9. Reproduced with
clang 20.1, 21.1, 22.1, 23.1 and trunk against libstdc++ 15.2, 16.2 and
trunk (Compiler Explorer with --gcc-toolchain=/opt/compiler-explorer/
gcc-16.2.0; locally clang 23.1 against a 16.2.0 build).

What happens: basic_format_parse_context::__check_dynamic_spec<_Ts...>
is declared in the class (format:406) and defined out of class near the
end of the header (format:5406), because its body needs
_Scanner::_Parse_context, _Arg_t and __to_arg_t_enum. In between,
formatter specializations for a fixed character type make non-dependent
calls. Clang resolves those as soon as it parses them, and because the
callee is constexpr it instantiates it right there:

  formatter<basic_string<char, _Traits, _Alloc>, char>::parse calls
    _M_f.parse(__pc) on a __formatter_str<char> (format:2718), which
    instantiates __formatter_str<char>::parse -> _Spec<char>::
    _M_parse_width -> _S_parse_width_or_precision ->
    basic_format_parse_context<char>::check_dynamic_spec_integral;
  formatter<char, wchar_t>::parse calls _M_f._M_parse<char>(__pc) on a
    __formatter_int<wchar_t> (format:2594) and instantiates the same
    chain for wchar_t.

check_dynamic_spec_integral (format:355) refers to __check_dynamic_spec<
int, unsigned, long long, unsigned long long> at that point, while the
template has no definition yet. Clang instantiates a constexpr function
template specialization when it is first referenced; when no definition
is available it retries only at the end of the translation unit and does
not instantiate on demand during constant evaluation, so every later
consteval evaluation of a format string with a dynamic width or precision
sees an "undefined function" (llvm/llvm-project issue 73232, open since
2023-11, labeled confirmed and libstdc++; the related core issue is
CWG2497). The header is correct: the point of instantiation at the end
of the translation unit is what the standard provides, and gcc, EDG 6.9
and MSVC 19 all instantiate on demand and accept the attached
reduce_a_late_definition.cc, which has the same shape without <format>;
clang trunk rejects it.

libstdc++ supports being used from clang, and for those users this makes
dynamic widths unusable in C++26 mode until clang moves. It therefore
seems worth arranging the header so that check_dynamic_spec_integral and
check_dynamic_spec_string do not depend on a template that is still
undefined at the point where clang instantiates them. One arrangement
that works:
opaque-declare `enum class _Arg_t : unsigned char;` before
basic_format_parse_context; move the `const _Arg_t* _M_types` member
from _Scanner::_Parse_context into basic_format_parse_context; have the
two functions do the id range check inline and then call non-template
helpers __format::__is_dynamic_spec_integral(_Arg_t) /
__is_dynamic_spec_string(_Arg_t), declared before the class and defined
right after the _Arg_t enumerators (an ordinary function may be defined
after its first use without any of this trouble). The generic
__check_dynamic_spec<_Ts...> can stay out of class: only user formatters
reach it, and they are compiled after the header is complete.

The attached libstdcxx-format-late-definition.diff does that against
trunk (r17-3940); it is what we ship locally for our clang + libstdc++
build, and the attached test passes with clang 23.1 and trunk g++ against
the patched trunk headers. It was prepared with LLM assistance (Claude,
Anthropic) and reviewed by me, so per https://gcc.gnu.org/ai-policy.html
I am flagging that here. It is about 45 lines; if that counts as legally
significant, please treat the diff as an illustration of the approach and
write your own. I can send it to libstdc++@ with a ChangeLog and
Assisted-by tag if that is preferred.

Notes: libc++ does not implement check_dynamic_spec yet and accepts the
test. testsuite/std/format/parse_ctx.cc exercises
check_dynamic_spec{,_integral,_string}, but the testsuite runs under g++,
which accepts the current header, so the attached test under clang is
the only way to see the problem.

Reply via email to