fmt::format vs fmt::print: The Core Difference in C++ String Formatting
fmt::format returns a formatted std::string for further use, while fmt::print writes formatted output directly to a stream or stdout without returning anything.
Both functions are part of the {fmt} library's public API, but they solve different problems in C++ string formatting. Understanding when to use each can eliminate unnecessary string allocations and improve your application's I/O performance. This guide breaks down their differences using the actual implementation in fmtlib/fmt.
Return Type and Basic Behavior
The most immediate distinction is what each function returns:
| Function | Return Type | Primary Action |
|---|---|---|
fmt::format |
std::string |
Constructs and returns a string object |
fmt::print |
void |
Performs side-effect output, returns nothing |
This difference shapes every other characteristic of these two APIs.
Implementation Details from the Source Code
fmt::format Implementation
The fmt::format function template is defined in include/fmt/format.h at lines 4600–4603:
template <typename... T>
FMT_NODISCARD FMT_INLINE auto format(format_string<T...> fmt, T&&... args)
-> std::string {
return vformat(fmt.str, vargs<T...>{{args...}});
}
This implementation:
- Calls
vformatinternally to produce the final string - Uses a temporary
detail::counting_bufferto determine the required size before allocating the result - Returns a newly constructed
std::stringcontaining the formatted output
fmt::print Implementation
The fmt::print function has two primary overloads. The convenience version that defaults to stdout lives in include/fmt/base.h at lines 2890–2892:
template <typename... T>
FMT_INLINE void print(format_string<T...> fmt, T&&... args) {
vprint(stdout, fmt.str, vargs<T...>{{args...}});
}
The generic overload accepting any FILE* is in include/fmt/format.h at lines 1084–1088:
inline void print(FILE* f, format_string<T...> fmt, T&&... args) {
if (detail::use_utf8) vprint(f, fmt.str, vargs<T...>{{args...}});
else vprint(f, fmt.str, vargs<T...>{{args...}});
}
Both overloads delegate to vprint or detail::print, which write directly to the output destination without creating an intermediate string object.
When to Use fmt::format
Use fmt::format when you need the formatted text for subsequent operations:
#include <fmt/format.h>
int main() {
// Build a message for logging system
std::string msg = fmt::format("The answer is {}.", 42);
// msg can be stored, modified, or passed to other APIs
logger.write(msg);
network.send(msg);
// String concatenation before final output
std::string header = fmt::format("[{}] ", timestamp());
std::string full_msg = header + msg;
}
Common use cases include:
- Preparing messages for logging frameworks
- Constructing JSON or XML payloads
- Building SQL queries with escaped parameters
- Formatting values for unit test assertions
- Creating strings that will be stored or transmitted
When to Use fmt::print
Use fmt::print when you want immediate output without retaining the text:
#include <fmt/print.h>
int main() {
// Direct console output—no string allocation
fmt::print("The answer is {}.\n", 42);
}
File output using fmt::output_file (from include/fmt/os.h):
#include <fmt/os.h>
int main() {
auto out = fmt::output_file("results.txt");
out.print("Processing complete: {} items\n", item_count);
// Writes directly, no intermediate std::string
}
Common use cases include:
- Console applications and CLI tools
- Writing progress indicators or status messages
- Log files where you don't need the string later
- Real-time data streams
- Any "fire-and-forget" output scenario
Performance Comparison
The performance difference stems from memory allocation behavior:
-
fmt::format— Always allocates memory for the returnedstd::string(unless you use the overload withfmt::basic_memory_buffer). This allocation and the resulting copy can be expensive in hot loops. -
fmt::print— Avoids the string allocation entirely when writing to an already-opened stream. The formatted data flows directly from the internal buffer to the output destination.
For high-frequency output (logging millions of lines, progress updates), prefer fmt::print. For batched or conditional output where you may not print at all, fmt::format gives you flexibility to decide later.
Thread Safety Considerations
Both functions are thread-safe in their internal operations, but the surrounding context differs:
-
fmt::format— The returned string is a self-contained value. Once constructed, it can be passed safely between threads with no synchronization concerns. -
fmt::print— Thread safety depends on the output destination.stdoutis typically synchronized by the C runtime library, butFILE*streams andstd::ostreamobjects may require external synchronization if shared across threads.
C API Equivalents
The {fmt} library exposes C-compatible versions of both functions:
| C++ Function | C Equivalent | Behavior |
|---|---|---|
fmt::format |
fmt_format |
Returns a newly allocated C string (caller must free) |
fmt::print |
fmt_print |
Writes directly to a FILE* |
These are useful when integrating {fmt} into C codebases or writing language bindings.
Summary
fmt::formatconstructs and returns astd::string—use it when you need the formatted text for storage, transmission, or further processingfmt::printwrites directly to output destinations withvoidreturn—use it for immediate display or file I/O without allocation overhead- The implementations in
include/fmt/format.handinclude/fmt/base.hshare formatting logic but diverge at the final step: string construction vs. direct output - Choose
fmt::printfor performance-critical output loops; choosefmt::formatwhen you need the string value itself
Frequently Asked Questions
Can I use fmt::print with std::ostream instead of FILE*?
Yes. Include include/fmt/ostream.h to get fmt::print overloads that accept std::ostream&. This allows integration with C++ streams while maintaining the same direct-output semantics.
Is there a way to use fmt::format without allocating memory?
Yes. Use the overload that takes fmt::basic_memory_buffer as an output parameter. This lets you provide your own buffer and avoid dynamic allocation for known-size formatting.
Why does fmt::print have two separate implementations in base.h and format.h?
The include/fmt/base.h version provides the minimal stdout-only functionality for lightweight use cases. The include/fmt/format.h version adds the full FILE* overload and integrates with the complete formatting system. This split reduces compile times when you only need basic functionality.
Can I mix fmt::format and fmt::print in the same program?
Absolutely. They are designed to work together. A common pattern is using fmt::format to prepare complex messages, then fmt::print to output them, as shown in the source analysis examples.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →