Migration between fmt and std::format
The two APIs are close enough that migration is mostly mechanical in either direction — the interesting part is the short list of things that don't port.
Mechanical mapping
| fmt | std | Notes |
|---|---|---|
fmt::format | std::format | Same signature, same spec grammar |
fmt::format_to | std::format_to | Same signature |
fmt::format_to_n | std::format_to_n | Same signature |
fmt::formatted_size | std::formatted_size | Same signature |
fmt::print | std::print | std::print needs C++23, not just C++20 |
fmt::formatter<T> | std::formatter<T> | Same parse/format shape, different namespace |
fmt::runtime | std::runtime_format | Same purpose, different name |
What does not port from fmt to std
- Named arguments — see Positional and named arguments;
std::formathas no equivalent syntax. fmt::join— see Ranges, tuples and containers; the standard's built-in range formatting covers some of the same ground but is less flexible about separators and per-element specs.- Color and styles — see Color and text styles; no standard equivalent exists.
- The ostream bridge — see ostream fallback formatting;
std::formatrequires a realstd::formatter<T>for every type. FMT_STRING— not needed forstd::format, since C++20'sconstevalchecking is unconditional there.- Printing to a
FILE*—fmt::print(FILE*, ...)has no standard counterpart;std::printtargets astd::ostream/stdoutonly.
What does not port from std to fmt
Very little. Code written against std::format moves to fmt by changing the header and namespace —
std::format becomes fmt::format, <format> becomes <fmt/format.h> — with no feature gap in that
direction, since fmt is a superset in practice.
Code that compiles against both
An alias header lets application code call one name and pick the backend behind a feature-test macro.
#if defined(__cpp_lib_format)
#include <format>
namespace fmtc = std;
#else
#include <fmt/format.h>
namespace fmtc = fmt;
#endif
Only the common subset — fmtc::format, fmtc::format_to, and so on — is usable through an alias
like this; reaching for a fmt-only feature breaks portability to the std branch immediately.
Custom formatters
A formatter<T> written for one is nearly source-compatible with the other — the parse/format
signatures match closely; only the namespace differs.
struct point_formatter_impl {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(const Point& p, auto& ctx) const {
return fmt::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
template <> struct fmt::formatter<Point> : point_formatter_impl {};
template <> struct std::formatter<Point> : point_formatter_impl {};
Which to prefer
std::format wins on "one fewer dependency" alone when it's available and sufficient. The moment you
need named arguments, fmt::join, color output, or support for a toolchain without std::format,
reach for fmt — it's the reference implementation, not a downgrade. See
Relationship to std::format for the full comparison.
See also
- Relationship to std::format — the lineage and feature comparison behind this migration guide.
- Common pitfalls — pitfalls that apply on both sides of a migration.
- formatter specialization — the full detail behind the shared-implementation pattern above.
- fmt overview — the full doc set.