{fmt} vs std::format: Should You Use the fmt Library in Modern C++?

Problem
C++ formatting has always felt awkward. Take a simple line: print a user name and a message count.
printf("User %s has %d messages\n", name.c_str(), count); // concise but not type-safe
std::cout << "User " << name << " has " << count << " messages\n"; // type-safe but verbose
fmt::print("User {} has {} messages\n", name, count); // concise + type-safe
The first line needs .c_str() and the %s/%d specifiers have no type guarantee. The second line is type-safe but noisy. The fmt::print line is both short and safe.
Now here is the real question: C++20 has std::format, C++23 has std::print. So why should you still learn {fmt}?
I kept seeing {fmt} in real projects even after adopting C++20 — mostly through spdlog. So I decided to answer the question properly: what {fmt} actually solves, how to use it, and when to pick it over the standard library.
What Is the {fmt} Library?
{fmt} (GitHub: fmtlib/fmt) is a modern, open-source C++ formatting library by Victor Zverovich, MIT-licensed and actively developed for over a decade. It is widely used in large C++ codebases, often indirectly through logging libraries like spdlog.
The important part is its relationship with the standard library: {fmt} implements most of the C++20 formatting library and C++23 std::print. Victor Zverovich is a major contributor to the related C++ standard proposals (P0645). But std::format is not literally “just fmt” — its implementations differ across compilers in features, performance, supported C++ versions, and extensions.
Installing {fmt}
There are three common ways, all driven by CMake.
1. FetchContent (pin a tested tag, do not track main):
include(FetchContent)
FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt GIT_TAG 12.2.0 # pin a released version)FetchContent_MakeAvailable(fmt)
target_link_libraries(myapp PRIVATE fmt::fmt)2. find_package (system packages, vcpkg, Conan, Homebrew):
find_package(fmt REQUIRED)target_link_libraries(myapp PRIVATE fmt::fmt)3. Header-only:
target_link_libraries(myapp PRIVATE fmt::fmt-header-only)Header-only is simple, but it raises compile cost in large projects. For medium or large projects I prefer the compiled library.
Now let me write something with it.
Your First fmt::format Example
#include <fmt/format.h>#include <string>
int main() { std::string msg = fmt::format("Hello, {}! You have {} new messages.", "Alice", 7); return 0;}{} is a replacement field. The formatting specifiers follow : inside the braces:
#include <fmt/format.h>
int main() { fmt::print("{:.2f}\n", 3.14159); // 3.14 fmt::print("{:08x}\n", 255); // 000000ff fmt::print("{:>10}\n", "hi"); // hi fmt::print("{:<10}\n", "hi"); // hi return 0;}Here is a small cheat sheet:
| Specifier | Meaning | Example |
|---|---|---|
{} | default formatting | 42 |
{:.2f} | float with 2 decimals | 3.14 |
{:08x} | hex, 8 wide, zero-padded | 000000ff |
{:>10} | right-aligned, width 10 | hi |
{:<10} | left-aligned, width 10 | hi |
fmt::print: A Better printf?
The real difference is not the brace style — it is type safety.
#include <fmt/format.h>
int main() { fmt::print("{}\n", 42); // ok: int fmt::format("{:d}", "hello"); // compile error return 0;}The second line fails at compile time because you asked for integer formatting (d) on a string. This matters for logging and server code: a bad format string in a hot error path becomes a build failure instead of undefined behavior or garbage output.
Compile-Time Format Checking
printf("%d\n", "hello") compiles fine and then does something wrong at runtime. {fmt} checks literal format strings at compile time through constexpr/consteval validation.
For dynamic format strings — built at runtime, for example from configuration — wrap them in fmt::runtime(...):
#include <fmt/format.h>#include <string>
int main() { std::string pattern = "Value: {}"; fmt::print(fmt::runtime(pattern), 42); // Value: 42 return 0;}My rule: keep compile-time checking unless you truly need runtime-generated format strings.
fmt/base.h vs fmt/format.h
Modern {fmt} 12.x is modular. Older advice telling you to #include <fmt/core.h> is outdated — do not use it.
| Header | What it gives you |
|---|---|
fmt/base.h | minimal formatting/printing, fewest dependencies |
fmt/format.h | full formatting (strings, specs, custom types) |
fmt/ranges.h | containers and tuples |
fmt/chrono.h | dates and times |
fmt/std.h | standard library types |
fmt/color.h | terminal colors |
fmt/os.h | file and system helpers |
fmt/compile.h | compile-time compiled format strings |
I include only what I use: fmt/base.h as the base, fmt/format.h when I need formatting, plus the extension headers on demand.
Formatting Containers and Ranges
#include <fmt/ranges.h>#include <fmt/format.h>#include <vector>#include <map>
int main() { std::vector<int> values{1, 2, 3}; fmt::print("{}\n", values); // [1, 2, 3]
std::map<std::string, int> scores{{"alice", 10}, {"bob", 8}}; fmt::print("{}\n", scores); // {"alice": 10, "bob": 8} return 0;}This is very useful for debug logging: no more hand-written loops just to print a vector.
Formatting Dates and Time
#include <fmt/chrono.h>#include <fmt/format.h>#include <chrono>
int main() { using namespace std::chrono;
auto now = system_clock::now(); fmt::print("{:%Y-%m-%d %H:%M:%S}\n", now); // 2026-09-04 10:41:40
fmt::print("{}\n", 250ms); // 250ms return 0;}This removes a lot of hand-rolled strftime and stream code for timestamps and durations.
Formatting Standard Library Types
#include <fmt/std.h>#include <fmt/format.h>#include <filesystem>#include <optional>
int main() { std::filesystem::path p = "/tmp/data.txt"; fmt::print("{}\n", p); // /tmp/data.txt
std::optional<int> maybe = 42; fmt::print("{}\n", maybe); // optional(42) return 0;}With fmt/std.h you can format std::filesystem::path, std::optional, std::variant, std::thread::id, and exception-related types directly. That means less manual conversion in logging code.
Custom Types with fmt::formatter
To format your own type, specialize fmt::formatter<T> with two methods: parse and format.
#include <fmt/format.h>
struct Point { double x; double y;};
template <>struct fmt::formatter<Point> { constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
auto format(const Point& p, format_context& ctx) const { return fmt::format_to(ctx.out(), "({}, {})", p.x, p.y); }};
int main() { Point p{1.5, 2.5}; fmt::print("Point: {}\n", p); // Point: (1.5, 2.5) return 0;}Why formatter<T> instead of operator<<?
- no
std::ostreampollution in your class, - supports format specs, so it integrates with the fmt ecosystem,
- works with
fmt::format,fmt::print, ranges, and logging libraries.
fmt::format_to and Avoiding Extra Allocations
fmt::format returns a new std::string. When you format repeatedly into a buffer — logging, serialization, network protocol generation — the extra allocation adds up. fmt::format_to writes into an output iterator instead:
#include <fmt/format.h>#include <string>#include <iterator>
int main() { std::string out; fmt::format_to(std::back_inserter(out), "request={} latency={}ms", 1042, 12); // out == "request=1042 latency=12ms" return 0;}For high-frequency formatting, this is the API I reach for.
Compile-Time Compiled Format Strings
FMT_COMPILE("...") moves format-string parsing to compile time:
#include <fmt/compile.h>#include <fmt/format.h>
int main() { std::string s = fmt::format(FMT_COMPILE("{}"), 42); return 0;}I warn against applying FMT_COMPILE everywhere: it produces more code. Reserve it for very hot paths such as high-frequency telemetry or logging.
Why Is {fmt} Fast?

The speed comes from design, not magic:
- efficient integer conversion,
- Dragonbox float conversion (shortest representation, correct rounding, round-trip),
- reduced dynamic allocation,
- avoiding iostream overhead (stream state, locale machinery, manipulators).
Type erasure shares runtime code across formatter instantiations, which controls template bloat, compile time, and binary size.
Performance Benchmarks: Read Them Carefully
The official fmt benchmarks show numbers like this on one specific run:
| Approach | Time |
|---|---|
| libc printf | 0.66 s |
| libc++ ostream | 1.63 s |
| fmt::print | 0.44 s |
| Boost.Format | 3.89 s |
| Folly | 1.28 s |
Read these with a caveat: results depend on compiler, flags, CPU, standard-library implementation, workload, and fmt version. Never claim “fmt is always 30x faster.” The engineering conclusion is simple — benchmark your own workload before deciding.
Compile Time and Binary Size
Type erasure keeps runtime code shared, but template-heavy formatting can still grow binaries. My practical advice:
- start with
fmt/base.hand add headers only when needed, - avoid unnecessary header-only usage in large projects,
- do not overuse
FMT_COMPILE, - monitor build time and binary size as you add formatters.
fmt vs printf vs iostreams vs std::format
| Feature | printf | iostreams | std::format | {fmt} |
|---|---|---|---|---|
| Type safety | no | yes | yes | yes |
| Format syntax | %s/%d | << | {} | {} |
| Compile-time checking | no | no | yes | yes |
| Custom types | no | operator<< | formatter | formatter |
| Ranges | no | partial | partial | yes (fmt/ranges.h) |
| Chrono | strftime | manual | partial | yes (fmt/chrono.h) |
| Colors | no | no | no | yes (fmt/color.h) |
| File helpers | fopen | ofstream | no | yes (fmt/os.h) |
| Older C++ support | C | C++98 | C++20+ | C++11+ |
| Ecosystem maturity | mature | mature | varies by compiler | mature |
One caution: std::format support varies by compiler and standard library, so “std::format” in the table really means “your toolchain’s implementation of it.”
fmt vs std::format: Which Should You Use in 2026?

This is the centerpiece, and there is no blanket “fmt is better.”
Prefer std::format/std::print when:
- your project is fixed on C++20/23 or newer,
- the standard library is mature on all your target compilers,
- you want fewer third-party dependencies,
- basic formatting is enough.
Prefer {fmt} when:
- you support C++11/14/17,
- you build across multiple compilers and platforms,
- you need ranges, colors, or OS helpers,
- you are already on the spdlog/fmt ecosystem,
- you want consistent cross-platform behavior,
- you already have large existing fmt usage,
- you want early access to new formatting features.
Consider migrating to the standard library when: a unified C++23 toolchain exists and you use fmt only for basic format/print, with no complex formatter extensions, and you value the dependency reduction.
My conclusion: use the standard library by default when it fully satisfies your requirements; use {fmt} when compatibility, extensions, performance tuning, or ecosystem maturity justify it.
Migration from printf
Before:
#include <cstdio>#include <string>
void log_user(const std::string& name, int count) { printf("User %s has %d messages\n", name.c_str(), count);}After:
#include <fmt/format.h>#include <string>
void log_user(const std::string& name, int count) { fmt::print("User {} has {} messages\n", name, count);}Benefits: no more .c_str() calls, automatic type matching, easier argument reordering, and more maintainable complex format strings.
Migration from iostreams
Before:
#include <iostream>#include <string>
void log_request(const std::string& id, int status, double ms) { std::cout << "request=" << id << " status=" << status << " time=" << ms << "ms\n";}After:
#include <fmt/format.h>#include <string>
void log_request(const std::string& id, int status, double ms) { fmt::print("request={} status={} time={:.1f}ms\n", id, status, ms);}The stream state, manipulators, and operator chains disappear. In my experience the readability gain usually outweighs any small performance difference.
Using {fmt} with Logging Libraries
spdlog builds on fmt, so everything above carries over:
#include <spdlog/spdlog.h>
int main() { spdlog::info("request={} latency={}ms", 1042, 12); return 0;}This is a common reason people end up with fmt without choosing it directly.
Common Mistakes
- Missing the include —
fmt::print("{}\n", values)on astd::vectorneeds#include <fmt/ranges.h>, not justfmt/format.h. - Treating runtime strings as compile-time format strings — use
fmt::runtime(...)when the string is dynamic; do not use it unnecessarily for literals. - Forgetting to link
fmt::fmt— linker errors likeundefined reference to fmt::v12::detail::vformat_tomean you missedtarget_link_libraries(myapp PRIVATE fmt::fmt). - Using outdated
fmt/core.hexamples — current blog posts and code should usefmt/base.horfmt/format.h. - Assuming all
std::formatimplementations behave exactly like fmt — features, performance, C++ version, and extensions differ per compiler.
Recommended Project Configuration
For a typical C++20 project:
#include <fmt/base.h> // minimal base#include <fmt/format.h> // full formatting#include <fmt/ranges.h> // containers (only if needed)find_package(fmt REQUIRED)target_link_libraries(myapp PRIVATE fmt::fmt)The principle: include only what you use.
When You Probably Do NOT Need {fmt}
If your project is fully C++23 with a uniform toolchain, only needs basic formatting, and std::format/std::print cover it — use the standard library. A single std::cout << value; line does not justify a new dependency. Being honest about this makes the rest of the article more credible.
Final Decision Table
| Your situation | Recommended choice |
|---|---|
| C++11/14/17 project | {fmt} |
| C++20 with inconsistent stdlib across compilers | {fmt} |
| C++23 modern greenfield, uniform toolchain | consider std::format/std::print first |
| Need ranges, colors, file helpers | {fmt} |
| Large existing fmt usage | keep {fmt} |
| Library wants minimum dependencies | consider the standard library |
| Performance-critical formatting | benchmark both, on your workload |
FAQ
Is fmt part of the C++ standard library?
No. {fmt} is a separate open-source library (fmtlib/fmt). The C++ standard library has its own formatting facilities, std::format (C++20) and std::print (C++23).
Is fmt the same as std::format?
Not exactly. {fmt} implements most of the C++20 formatting library, but it lives in the fmt namespace, offers extensions like ranges, chrono, colors, and OS helpers, and supports C++11 and newer. std::format implementations vary by compiler.
Is fmt faster than std::format?
Sometimes, for some workloads. The official fmt benchmarks show substantial advantages for some patterns, but results depend on compiler, standard library, and formatting pattern. Measure on your own workload.
Does fmt require C++20?
No. {fmt} supports C++11 and newer, which is one of its main advantages over std::format.
Is fmt header-only?
It can be. Use fmt::fmt-header-only or define FMT_HEADER_ONLY. For medium or large projects, the compiled library is usually a better choice.
How do I format a vector with fmt?
Include fmt/ranges.h and print the container directly:
#include <fmt/ranges.h>#include <fmt/format.h>#include <vector>
int main() { fmt::print("{}\n", std::vector{1, 2, 3}); // [1, 2, 3] return 0;}How do I format a custom C++ class with fmt?
Specialize fmt::formatter<T> with parse and format methods, then use fmt::print("{}", obj).
Should new C++23 projects still use fmt?

It depends. If a uniform C++23 toolchain and basic formatting are enough, std::format/std::print are a reasonable default. Choose {fmt} when you need compatibility with older C++, richer extensions, or consistent cross-platform behavior.
The Reason {fmt} Is Still Relevant
I think the key reason the question exists is that the standard library finally caught up — but only recently and only partially:
Format string → compile-time validation → type-erased arguments → formatter → buffer/outputWhen C++20 shipped std::format, many developers assumed {fmt} would become obsolete. What I found is the opposite: {fmt} is the reference implementation and often the fastest, most consistent option, especially when you cannot standardize on one C++23 toolchain. std::format is a great default when it fully covers your case; {fmt} remains relevant for everything around it.
Summary
In this post, I answered the question “should you still use {fmt} when std::format exists” with a practical, decision-driven walkthrough: installing {fmt} with CMake, formatting strings, containers, chrono, and custom types, using format_to and FMT_COMPILE, and comparing {fmt} with printf, iostreams, and the standard library. The key point is: use the standard library by default when it fully satisfies your requirements; use {fmt} when compatibility, extensions, performance tuning, or ecosystem maturity justify it. Before you decide, benchmark on your own workload.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 fmt: A modern formatting library for C++
- 👨💻 fmt 12.0 API reference
- 👨💻 fmt GitHub repository (fmtlib/fmt)
- 👨💻 The fmt syntax documentation
- 👨💻 The fmt benchmarks
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments