Table of Contents

  1. FetchContent Integration
  2. Header-Only vs Compiled Mode
  3. FMT_HEADER_ONLY Flag
  4. Transition to C++20 std::format
  5. fmt with spdlog Bundling
  6. Version Compatibility
  7. Compile-Time Format Checking
Back to CMake Mastery Series

fmt Library

June 4, 2026 Wasil Zafar 7 min read

The complete guide to integrating the fmt formatting library with CMake — from FetchContent setup and header-only mode to C++20 std::format migration and spdlog coordination.

Formatting

FetchContent Integration

The fmt library is the reference implementation behind C++20's std::format. It provides safe, fast text formatting with an expressive mini-language. FetchContent is the recommended way to add fmt to a CMake project:

# CMakeLists.txt — fmt via FetchContent
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)

FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        11.0.2
)
FetchContent_MakeAvailable(fmt)

add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)
// src/main.cpp — Basic fmt usage
#include <fmt/core.h>
#include <fmt/color.h>
#include <fmt/ranges.h>
#include <vector>

int main() {
    // Basic formatting
    fmt::print("Hello, {}!\n", "world");

    // Positional arguments
    fmt::print("{1} is {0}\n", "great", "fmt");

    // Format specifications
    fmt::print("{:>20}\n", "right-aligned");
    fmt::print("{:.3f}\n", 3.14159);
    fmt::print("{:#010x}\n", 255);

    // Colored output
    fmt::print(fg(fmt::color::green), "Success!\n");

    // Container formatting
    std::vector<int> v = {1, 2, 3, 4, 5};
    fmt::print("Vector: {}\n", v);

    return 0;
}
Key Insight: The fmt::fmt target automatically handles include directories and compile flags. Unlike many libraries, fmt's CMakeLists.txt is exemplary — it exports proper targets, supports find_package in config mode, and works seamlessly with both FetchContent and system installs.

Header-Only vs Compiled Mode

fmt can operate in two modes: compiled (default) where format functions are in a separate translation unit, or header-only where everything is inlined. Each mode has distinct trade-offs:

# Compiled mode (default) — faster compile times for large projects
FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        11.0.2
)
FetchContent_MakeAvailable(fmt)

# Links against the compiled libfmt
target_link_libraries(myapp PRIVATE fmt::fmt)
# Header-only mode — single-TU projects or when avoiding link dependencies
FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        11.0.2
)
FetchContent_MakeAvailable(fmt)

# Use header-only target — no library to link
target_link_libraries(myapp PRIVATE fmt::fmt-header-only)

The compiled mode produces a static library (libfmt.a / fmt.lib) containing the implementation of core formatting functions. This means format function definitions are compiled once, reducing total build time when fmt is used across many translation units.

Pitfall: Never mix fmt::fmt and fmt::fmt-header-only in the same binary. Linking one target that uses the compiled library with another that uses header-only will produce duplicate symbol errors or ODR violations.

FMT_HEADER_ONLY Flag

Before fmt provided the fmt::fmt-header-only target, projects used the FMT_HEADER_ONLY macro. This approach still works but the target-based method is preferred:

# Legacy approach — still functional but prefer fmt::fmt-header-only
FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        11.0.2
)
FetchContent_MakeAvailable(fmt)

add_executable(myapp src/main.cpp)
target_include_directories(myapp PRIVATE ${fmt_SOURCE_DIR}/include)
target_compile_definitions(myapp PRIVATE FMT_HEADER_ONLY=1)
# Modern equivalent — use the provided target
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt-header-only)
# The target sets FMT_HEADER_ONLY automatically

Transition to C++20 std::format

fmt is the basis for C++20's <format> header. You can write code that compiles against either implementation, easing migration as compiler support matures:

# CMakeLists.txt — Conditional fmt vs std::format
cmake_minimum_required(VERSION 3.20)
project(FormatApp LANGUAGES CXX)

# Try C++20 std::format first
include(CheckIncludeFileCXX)
set(CMAKE_REQUIRED_FLAGS "-std=c++20")
check_include_file_cxx("format" HAS_STD_FORMAT)

if(HAS_STD_FORMAT)
    set(CMAKE_CXX_STANDARD 20)
    message(STATUS "Using std::format (C++20)")
    add_executable(myapp src/main.cpp)
    target_compile_definitions(myapp PRIVATE USE_STD_FORMAT=1)
else()
    set(CMAKE_CXX_STANDARD 17)
    message(STATUS "Using fmt library (fallback)")

    include(FetchContent)
    FetchContent_Declare(fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt.git
        GIT_TAG        11.0.2
    )
    FetchContent_MakeAvailable(fmt)

    add_executable(myapp src/main.cpp)
    target_link_libraries(myapp PRIVATE fmt::fmt)
    target_compile_definitions(myapp PRIVATE USE_FMT_LIB=1)
endif()
// src/format_compat.h — Compatibility header
#pragma once

#if defined(USE_STD_FORMAT)
    #include <format>
    #include <print>
    namespace fmtlib = std;
#elif defined(USE_FMT_LIB)
    #include <fmt/core.h>
    #include <fmt/format.h>
    namespace fmtlib = fmt;
#endif

// Usage: fmtlib::format("Hello, {}!", name)
// Usage: fmtlib::print("Value: {}\n", 42)

fmt with spdlog Bundling

spdlog bundles its own copy of fmt by default. When using both libraries, you must coordinate to avoid version conflicts and duplicate symbols:

# CMakeLists.txt — Shared fmt between spdlog and application
cmake_minimum_required(VERSION 3.20)
project(LogApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

include(FetchContent)

# Fetch fmt first
FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        11.0.2
)
FetchContent_MakeAvailable(fmt)

# Tell spdlog to use our external fmt
set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "Use external fmt" FORCE)

FetchContent_Declare(spdlog
    GIT_REPOSITORY https://github.com/gabime/spdlog.git
    GIT_TAG        v1.14.1
)
FetchContent_MakeAvailable(spdlog)

add_executable(logapp src/main.cpp)
target_link_libraries(logapp PRIVATE
    spdlog::spdlog
    fmt::fmt
)
Key Insight: Always set SPDLOG_FMT_EXTERNAL ON when both fmt and spdlog are in the same build. Without this, spdlog compiles its bundled fmt copy, causing ODR violations when your code also links fmt::fmt. The versions must be compatible — check spdlog's release notes for supported fmt ranges.

Version Compatibility

fmt occasionally introduces breaking changes between major versions. Pin your version carefully and understand the compatibility landscape:

# Version pinning strategies
include(FetchContent)

# Strategy 1: Exact version pin (most reproducible)
FetchContent_Declare(fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG        11.0.2   # Exact tag
)

# Strategy 2: Version range with find_package fallback
find_package(fmt 10.0..<12.0 QUIET)
if(NOT fmt_FOUND)
    FetchContent_Declare(fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt.git
        GIT_TAG        11.0.2
    )
    FetchContent_MakeAvailable(fmt)
endif()

# Strategy 3: System package preferred, FetchContent fallback
find_package(fmt 10.0 QUIET CONFIG)
if(NOT fmt_FOUND)
    message(STATUS "System fmt not found, fetching from source")
    FetchContent_Declare(fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt.git
        GIT_TAG        11.0.2
    )
    FetchContent_MakeAvailable(fmt)
endif()

Compile-Time Format Checking

fmt validates format strings at compile time (since fmt 8.0+), catching mismatches between format specifiers and arguments before the program runs:

// src/main.cpp — Compile-time format string validation
#include <fmt/core.h>
#include <fmt/compile.h>
#include <string>

int main() {
    // These are checked at compile time (fmt 8.0+)
    std::string result = fmt::format("{} + {} = {}", 1, 2, 3);
    fmt::print("{:.2f}\n", 3.14159);

    // FMT_COMPILE for maximum performance — format string parsed at compile time
    auto compiled = fmt::format(FMT_COMPILE("{} items at ${:.2f} each"), 5, 9.99);
    fmt::print("{}\n", compiled);

    // This would cause a compile error:
    // fmt::format("{:d}", "not a number");  // Error: invalid format specifier
    // fmt::format("{} {}", 1);              // Error: argument count mismatch

    return 0;
}
# Enable compile-time checking (automatic in fmt 8.0+)
# For older fmt versions or extra strictness:
target_compile_definitions(myapp PRIVATE
    FMT_ENFORCE_COMPILE_STRING  # Force all format strings to be compile-time checked
)
Key Insight: FMT_COMPILE goes beyond validation — it parses the format string at compile time and generates optimized formatting code. Benchmarks show 2-10x speedup over runtime parsing for simple format strings, making it ideal for hot paths like logging.