If you try to compile C++23 code that uses std::format on an older toolchain,
you may hit:
/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
git submodule add https://github.com/fmtlib/fmt.git dependencies/fmt
cd dependencies/fmt
git checkout 11.1.4
cd ../..
git add dependencies/fmt .gitmodulesCreate a std::format shim header
Create include/yourproject/fmt_shim/format (the file is literally named format
so that #include <format> resolves to it):
#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:
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:
#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
| Header | Fallback submodule | Native since | Shim header |
|---|---|---|---|
<format> | dependencies/fmt | GCC 13 | fmt_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.