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;
}
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.
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
)
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
)
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.