Skip to content

C++26 build: fmt/optional vs fmt/ranges ambiguity when libstdc++-16 makes std::optional a range #3411

@travisdowns

Description

@travisdowns

Symptom

Building seastar with -std=c++26 on clang-20+/gcc-16 against libstdc++-16 (Ubuntu 26.04) fails with the cooked fmt 11.2.0 (and also with the recently released fmt 12.0.0 / 12.1.0). Affected TU in our matrix is tests/unit/sstring_test.cc:

fmt/base.h:2262:45: error: implicit instantiation of undefined template
  'fmt::detail::type_is_unformattable_for<std::optional<seastar::basic_sstring<char, unsigned int, 15>>, char>'

sstring_test.cc:322:
  std::ignore = fmt::format("{}", std::optional(sstring{"hello"}));

A minimal local probe shows the real issue is ambiguous partial specializations:

error: ambiguous partial specializations of
  'formatter<std::optional<seastar::basic_sstring<char, unsigned int, 15>>>'
  fmt/ranges.h:491: partial specialization matches [with R = std::optional<...>, Char = char]
  fmt/std.h:217:   partial specialization matches [with T = ..., Char = char]

is_formattable<std::optional<sstring>> therefore evaluates false, which is what surfaces as the cryptic type_is_unformattable_for error at the call site.

Root cause

libstdc++-16 implements P3168R2 (std::optional range support) for C++26, so std::optional<T> now models std::ranges::range. fmt 11.2.0 / 12.0.0 / 12.1.0 have two unconstrained formatter partial specializations that both match std::optional<T> once it's a range:

  1. fmt/std.h — formatter for std::optional<T> gated only on is_formattable<T> (correct).
  2. fmt/ranges.h — generic range formatter that matches any type whose range_format_kind is non-disabled, including the new optional-as-range.

This is fmtlib/fmt#4760, fixed by fmtlib/fmt#4761 (merged 2026-05-03, master commit 9cb8c0f92b4c345fb974a75d71370c23047528aa). No tagged fmt release carries the fix yet (12.1.0 was 2025-10-29).

Reproduction

Run inside an ubuntu:26.04 container:

apt-get update
apt-get install -y --no-install-recommends g++ libfmt-dev clang-22 libstdc++-16-dev
cat > /tmp/t.cc <<EOF
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <fmt/std.h>
#include <optional>
#include <string>
int main() {
    using F = fmt::formatter<std::optional<std::string>>;
    F f;
    (void)f;
}
EOF
clang++-22 -std=gnu++26 /tmp/t.cc -c -o /tmp/t.o

Substitute seastar::basic_sstring for std::string to see the in-tree variant.

Workaround in our CI branch

A new fmt-12-1-dev cooking ingredient is added in cooking_recipe.cmake, pinned to the post-12.1.0 fix commit. C++26 matrix entries pass --cook fmt-12-1-dev instead of the default --cook fmt (which still pulls 11.2.0).

Tracking goal

When fmt ships a tagged release containing fmtlib/fmt#4761, bump the default cooking_ingredient (fmt ...) URL in cooking_recipe.cmake to that tag and delete fmt-12-1-dev.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions