Compare commits

...

6 Commits

Author SHA1 Message Date
hexonal
c038937ddd
Merge 841a3c37f0edd2a2b15c146489b665a226a08ae5 into 17500e457b198c37c4374d142fa7a1d36e4651fe 2026-07-29 10:29:00 -07:00
rimathia
17500e457b
doc extension concerning format_as and general implementations (#4857) 2026-07-29 09:43:31 -07:00
Victor Zverovich
26c01df3bd Handle const-qualified named arguments
A named argument that arrives const-qualified was not recognized by
is_named_arg, silently dropping its name. This happens both when a
named argument is passed through an intermediate function returning
const T& (https://github.com/fmtlib/fmt/issues/4866) and in fmt's own
compiled format path, which passes arguments as const T&.

Make is_named_arg and is_static_named_arg see through top-level const so
the name is preserved instead of dropped.
2026-07-29 09:20:23 -07:00
hexonal
841a3c37f0
Merge branch 'main' into fix-linux-ci-cc-cxx-mismatch 2026-07-23 13:03:04 +08:00
hexonal
2cd3227b89 Skip c-test on clang++-3.6 due to pre-3.8 _Generic decay bug
Now that CC correctly matches CXX for this job, test/c-test.c is
compiled with real Clang 3.6.2 for the first time, which fails on
string-literal arguments to fmt's C API macros ("too many arguments
to function call, expected 0, have 1").

Root cause: fmt-c.h's FMT_MAKE_ARG uses a C11 _Generic dispatch with
a 0-arg fmt_unsupported_type default. Whether the controlling
expression of _Generic undergoes array-to-pointer decay was
genuinely ambiguous in C11 wording (WG14 N1930); Clang treated it as
an unconverted lvalue and did not decay arrays until the fix for
LLVM PR16340 landed in November 2015 (clang 3.8). Clang 3.6.2
(Feb 2015) predates that fix, so a string literal like "foo" (type
char[4]) never matches the char*/const char* cases and falls
through to the default, which then gets called with an argument.

Verified this is specific to old Clang, not a general fmt-c.h bug:
- Compiled test/c-test.c against fmt-c.h with a modern Clang
  locally; it builds cleanly, confirming decay works correctly once
  PR16340 is fixed.
- Confirmed no safe macro-level workaround exists: forcing decay
  uniformly (e.g. `(x) + 0` or `1 ? (x) : (x)`) triggers ordinary
  integer promotion for the small-integer cases (char, bool, short,
  etc.), silently routing them to the wrong fmt_from_* function on
  *all* compilers. A per-type-conditional wrapper isn't expressible
  without already knowing the type, i.e. without _Generic itself.

Given this is a real, long-since-fixed upstream compiler bug in a
10-year-old Clang release, and no low-risk fix at the fmt-c.h level
exists that doesn't risk changing dispatch behavior on every other
compiler, skip just c-test (via ctest -E) for the clang++-3.6 job.
The C++ test suite for that job is unaffected and still runs.

The exclude value must be double-quoted when assigned from the
templated expression, since GitHub Actions substitutes it inline and
an unquoted assignment containing a space (e.g. exclude=-E ^c-test$)
does not parse as a single shell assignment - it splits into a
variable-scoped-to-command form that tries to execute a nonexistent
'^c-test$' command, aborting the step under bash's default -e.
Verified both branches (clang++-3.6 and every other matrix entry) by
actually executing the resulting snippet with bash -eo pipefail.
2026-07-22 22:20:53 -04:00
flink
3f3b503fee Set matching CC alongside CXX in Linux CI matrix
The Configure step only set CXX, so CMake's C compiler detection
fell back to whatever the default happened to be on the runner,
independent of which C++ compiler the matrix entry was actually
testing (e.g. CXX=clang++-3.6 but CC left to detect GCC 11).

Derive CC from the same matrix.cxx value the job already installs
a matching compiler for.
2026-07-20 21:16:49 -04:00
4 changed files with 42 additions and 6 deletions

View File

@ -13,6 +13,10 @@ jobs:
${{matrix.shared && 'Shared' || ''}}
${{matrix.cxxflags_extra && 'Sanitize' || ''}}
runs-on: ${{ matrix.os || 'ubuntu-22.04' }}
env:
# Run tests in a non-UTC timezone. glibc localtime/strftime honor TZ, so
# there is no need for timedatectl, which fails without systemd-timedated.
TZ: Europe/Kyiv
strategy:
matrix:
cxx: [g++-4.9, g++-11, clang++-3.6, clang++-11]
@ -75,15 +79,14 @@ jobs:
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set timezone
run: sudo timedatectl set-timezone 'Europe/Kyiv'
- name: Install GCC 4.9
run: |
sudo apt update
sudo apt install libatomic1 libc6-dev libgomp1 libitm1 libmpc3
# https://launchpad.net/ubuntu/xenial/amd64/g++-4.9/4.9.3-13ubuntu2
wget --no-verbose \
# launchpad periodically returns 503s, so retry to avoid flaky CI.
wget --no-verbose --tries=5 --waitretry=10 \
--retry-connrefused --retry-on-http-error=503 \
http://launchpadlibrarian.net/230069137/libmpfr4_3.1.3-2_amd64.deb \
http://launchpadlibrarian.net/253728424/libasan1_4.9.3-13ubuntu2_amd64.deb \
http://launchpadlibrarian.net/445346135/libubsan0_5.4.0-6ubuntu1~16.04.12_amd64.deb \
@ -114,7 +117,9 @@ jobs:
sudo apt update
sudo apt install libtinfo5
# https://code.launchpad.net/ubuntu/xenial/amd64/clang-3.6/1:3.6.2-3ubuntu2
wget --no-verbose \
# launchpad periodically returns 503s, so retry to avoid flaky CI.
wget --no-verbose --tries=5 --waitretry=10 \
--retry-connrefused --retry-on-http-error=503 \
http://launchpadlibrarian.net/230019046/libffi6_3.2.1-4_amd64.deb \
http://launchpadlibrarian.net/445346109/libasan2_5.4.0-6ubuntu1~16.04.12_amd64.deb \
http://launchpadlibrarian.net/445346135/libubsan0_5.4.0-6ubuntu1~16.04.12_amd64.deb \
@ -174,6 +179,13 @@ jobs:
sudo apt install locales-all
cmake -E make_directory ${{runner.workspace}}/build
- name: Select matching C compiler
run: |
cc=${{matrix.cxx}}
cc=${cc/clang++/clang}
cc=${cc/g++/gcc}
echo "CC=$cc" >> "$GITHUB_ENV"
- name: Configure
working-directory: ${{runner.workspace}}/build
env:
@ -199,6 +211,18 @@ jobs:
- name: Test
working-directory: ${{runner.workspace}}/build
run: ctest -C ${{matrix.build_type}}
# clang++-3.6 predates the fix for LLVM PR16340 (the controlling
# expression of _Generic wasn't decayed/qualifier-stripped before
# type matching, fixed upstream in clang 3.8). That makes
# FMT_MAKE_ARG's _Generic dispatch in fmt-c.h fail to match string
# literals (e.g. "foo", of type char[4]) against the char*/const
# char* cases, so they fall through to the 0-arg default case and
# the call is passed too many arguments. This is a real, long-fixed
# upstream compiler bug rather than a bug in fmt's macro, so only
# c-test is skipped here for this one job; see the discussion on
# https://github.com/fmtlib/fmt/pull/4863 for the full analysis.
run: |
exclude="${{ matrix.cxx == 'clang++-3.6' && '-E ^c-test$' || '' }}"
ctest -C ${{matrix.build_type}} $exclude
env:
CTEST_OUTPUT_ON_FAILURE: True

View File

@ -92,6 +92,10 @@ Use `format_as` if you want to make your type formattable as some other
type with the same format specifiers. The `format_as` function should
take an object of your type and return an object of a formattable type.
It should be defined in the same namespace as your type.
`format_as` cannot be used when a type also matches another `formatter`
specialization, such as the range `formatter`, because the specializations
would be ambiguous. Disable the conflicting specialization, if possible,
or provide an explicit `formatter` specialization instead.
Example ([run](https://godbolt.org/z/nvME4arz8)):

View File

@ -1059,6 +1059,10 @@ template <typename T> struct is_static_named_arg : std::false_type {};
template <typename T, typename Char>
struct is_named_arg<named_arg<T, Char>> : std::true_type {};
template <typename T> struct is_named_arg<const T> : is_named_arg<T> {};
template <typename T>
struct is_static_named_arg<const T> : is_static_named_arg<T> {};
template <typename T, typename Char = char> struct named_arg : view {
const Char* name;
const T& value;

View File

@ -163,6 +163,10 @@ TEST(compile_test, named) {
fmt::format(FMT_COMPILE("{a0} {a1}"), "a0"_a = 41, "a1"_a = 43));
EXPECT_EQ("41 43",
fmt::format(FMT_COMPILE("{a1} {a0}"), "a0"_a = 43, "a1"_a = 41));
// A statically-named argument with a format spec compiles to spec_field,
// which passes the argument to make_format_args as const (#4866).
EXPECT_EQ("4.2", fmt::format(FMT_COMPILE("{arg:3.1f}"), "arg"_a = 4.2));
# endif
}