How to auto-detect and polyfill std::format on GCC 11 using {fmt}

If you try to compile C++23 code that uses std::format on an older toolchain, you may hit:

gcc_format_error.txt
/home/uli/Tether/src/gcode/analysis/OverhangAnalyzer.cpp:12:10: fatal error: format: No such file or directory
   12 | #include <format>
      |          ^~~~~~~~
compilation terminated.

This happens because <format> is only available in libstdc++ from GCC 13. GCC 11 and 12, including the default compiler on Ubuntu 22.04 (GCC 11.4), ship no <format> header at all — not even as <experimental/format>.

The cleanest fix is to fall back to the {fmt} library: it is the implementation that std::format was based on, so a thin namespace shim makes existing #include <format> / std::format(...) call sites compile unchanged.

Add {fmt} as a submodule

add_fmt_submodule.sh
git submodule add https://github.com/fmtlib/fmt.git dependencies/fmt
cd dependencies/fmt
git checkout 11.1.4
cd ../..
git add dependencies/fmt .gitmodules

Create a std::format shim header

Create include/yourproject/fmt_shim/format (the file is literally named format so that #include <format> resolves to it):

include/yourproject/fmt_shim/format
#pragma once
#include <fmt/format.h>

namespace std {
    using fmt::format;
    using fmt::format_to;
    using fmt::format_to_n;
    using fmt::vformat;
    using fmt::vformat_to;
    using fmt::format_string;
    using fmt::format_error;
    using fmt::format_args;
    using fmt::make_format_args;
    using fmt::format_context;
    using fmt::format_parse_context;
    using fmt::appender;
    using fmt::format_to_n_result;
} // namespace std

Auto-detect <format> in CMake

Place this near the top of your CMakeLists.txt, after the project() call and after setting CMAKE_CXX_STANDARD:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(CheckIncludeFileCXX)
set(CMAKE_REQUIRED_FLAGS "-std=c++23")
check_include_file_cxx("format" HAVE_NATIVE_STD_FORMAT)
unset(CMAKE_REQUIRED_FLAGS)

if(NOT HAVE_NATIVE_STD_FORMAT)
    message(STATUS "Native <format> not available; using {fmt} shim.")

    # Validate submodule
    set(FMT_PATH "${CMAKE_CURRENT_SOURCE_DIR}/dependencies/fmt")
    if(NOT EXISTS "${FMT_PATH}/include/fmt/format.h")
        message(FATAL_ERROR
            "fmt submodule missing. Run: git submodule update --init --recursive")
    endif()

    # Header-only mode means no extra library to link
    add_compile_definitions(FMT_HEADER_ONLY)

    # Shim must be searched BEFORE system paths so #include <format> finds it
    include_directories(BEFORE
        "${CMAKE_CURRENT_SOURCE_DIR}/include/yourproject/fmt_shim")
    include_directories("${FMT_PATH}/include")
else()
    message(STATUS "Native <format> available; using it directly.")
endif()

Keep source code unchanged

Your existing code can continue to use the standard spelling:

example.cpp
#include <format>
#include <iostream>
#include <string>

int main() {
    std::string s = std::format("pi = {:.2f}", 3.14159);
    std::cout << s << '\n';   // pi = 3.14
    return 0;
}

On GCC 15 this compiles with the native <format>. On GCC 11 the same file compiles through the {fmt} shim.

How it works

include_directories(BEFORE ...) prepends the shim directory to the compiler’s include search path. When the compiler sees #include <format>, it finds the shim header instead of the (missing) system header. The shim re-exports {fmt}’s APIs under namespace std, so the rest of the project never needs to know whether the real <format> or the polyfill is being used.

FMT_HEADER_ONLY disables {fmt}’s compiled library; this is simplest for a drop-in replacement and avoids an extra link target.

Summary

HeaderFallback submoduleNative sinceShim header
<format>dependencies/fmtGCC 13fmt_shim/format

This approach keeps your project portable across GCC 11 through the latest release without sprinkling #ifdef guards or fmt:: names through the code.


Check out similar posts by category: C/C++ C++23 CMake