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 vformat internally to produce the final string
  • Uses a temporary detail::counting_buffer to determine the required size before allocating the result
  • Returns a newly constructed std::string containing 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 returned std::string (unless you use the overload with fmt::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. stdout is typically synchronized by the C runtime library, but FILE* streams and std::ostream objects 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::format constructs and returns a std::string—use it when you need the formatted text for storage, transmission, or further processing
  • fmt::print writes directly to output destinations with void return—use it for immediate display or file I/O without allocation overhead
  • The implementations in include/fmt/format.h and include/fmt/base.h share formatting logic but diverge at the final step: string construction vs. direct output
  • Choose fmt::print for performance-critical output loops; choose fmt::format when 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →