Home C / C++ C++20/23 Format Library: Modern String Formatting in C++
Intermediate 3 min · July 13, 2026

C++20/23 Format Library: Modern String Formatting in C++

Master C++20/23 format library with practical examples.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C++ syntax and standard library.
  • Familiarity with templates and function overloading.
  • Understanding of strings and I/O.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • std::format provides type-safe, Python-like string formatting.
  • Supports positional and named arguments, width, precision, fill/align.
  • Custom formatters for user-defined types.
  • Faster and safer than printf and iostreams.
  • Available in C++20, with enhancements in C++23.
✦ Definition~90s read
What is C++20/23 Format Library?

std::format is a type-safe, extensible string formatting function introduced in C++20 that uses Python-like placeholders.

Think of std::format like a fill-in-the-blank template.
Plain-English First

Think of std::format like a fill-in-the-blank template. You write a string with placeholders like {} and provide values to insert. It automatically handles types and formatting, like setting a table with labeled slots for dishes.

String formatting is a fundamental task in programming, used for logging, user interfaces, data serialization, and debugging. For decades, C++ developers relied on printf (inherited from C) or iostreams (std::cout, std::stringstream). Both have drawbacks: printf is type-unsafe and error-prone; iostreams are verbose and slow. The C++20 standard introduced the format library (std::format), inspired by Python's str.format and the {fmt} library. It combines the best of both worlds: type safety, performance, and expressive syntax. C++23 further improves it with std::print and std::println. This tutorial covers everything you need to use the format library effectively in production code, from basic usage to custom formatters and debugging.

Basic Usage of std::format

The std::format function takes a format string and a variable number of arguments. Placeholders are denoted by curly braces {}. The simplest form is {} which outputs the argument in its default format. Arguments are indexed automatically starting from 0. You can also use explicit indices like {0}, {1} to reorder arguments. Named arguments are not supported directly, but you can use std::format with a map or tuple.

Example: std::format("Hello, {}!", "world") returns "Hello, world!".

Format specifiers follow a colon after the index: {0:spec}. Common specifiers include d for decimal, x for hex, f for fixed-point, e for scientific, and s for string. Width and precision are specified as {0:10} (minimum width 10) or {0:.2f} (2 decimal places). Alignment is done with <, >, ^ for left, right, center.

basic_format.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <format>
#include <iostream>

int main() {
    std::string name = "Alice";
    int age = 30;
    double pi = 3.1415926535;

    // Basic
    std::cout << std::format("Name: {}, Age: {}\n", name, age);

    // Positional
    std::cout << std::format("{1}, {0}\n", name, age);

    // Width and alignment
    std::cout << std::format("|{:10}|{:>10}|\n", name, age);

    // Precision
    std::cout << std::format("Pi: {:.2f}\n", pi);

    // Hex
    std::cout << std::format("Hex: {:x}\n", 255);

    return 0;
}
Output
Name: Alice, Age: 30
30, Alice
|Alice | 30|
Pi: 3.14
Hex: ff
🔥Include Header
📊 Production Insight
Always use std::format instead of sprintf to avoid buffer overflows.
🎯 Key Takeaway
std::format is type-safe and supports positional arguments and format specifiers.

Format Specifiers in Depth

Format specifiers follow the pattern: [[fill]align][sign][#][0][width][.precision][type]. Fill character defaults to space. Align: < (left), > (right), ^ (center). Sign: + (always show sign), - (only negative), space (space for positive). # adds alternate form (e.g., 0x for hex). 0 pads with zeros. Width is minimum field width. Precision for floating-point is number of digits after decimal; for strings, maximum length. Type: d, i, u, o, x, X, f, F, e, E, g, G, a, A, s, c, p.

Examples
  • {:>10} right-align in width 10
  • {:<+10.2f} left-align, sign, width 10, 2 decimals
  • {:#x} hex with 0x prefix
  • {:010} zero-padded width 10
  • {:.5s} string truncated to 5 chars
format_specifiers.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <format>
#include <iostream>

int main() {
    int n = 42;
    double d = -3.14159;
    std::string s = "Hello, World!";

    // Sign and fill
    std::cout << std::format("{:+05d}\n", n);   // +0042
    std::cout << std::format("{:05d}\n", n);    // 00042
    std::cout << std::format("{:<+10.2f}\n", d); // -3.14     

    // Alternate forms
    std::cout << std::format("{:#x}\n", 255);   // 0xff
    std::cout << std::format("{:#o}\n", 255);   // 0377

    // String precision
    std::cout << std::format("{:.5s}\n", s);    // Hello

    // Center
    std::cout << std::format("{:^20}\n", "centered");

    return 0;
}
Output
+0042
00042
-3.14
0xff
0377
Hello
centered
💡Precision for Strings
📊 Production Insight
Use zero-padding for fixed-width numeric fields in logs to align columns.
🎯 Key Takeaway
Format specifiers give fine control over output appearance.

Formatting User-Defined Types

To format custom types, specialize std::formatter for your type. The specialization must provide two member functions: parse() and format(). parse() reads the format specifier from the format string (between { and :). format() writes the formatted output to the output iterator. The parse method should return an iterator past the parsed specifier. The format method uses the specifier to format the value.

Example: Format a Point struct as (x, y). Support precision for coordinates.

custom_formatter.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <format>
#include <iostream>

struct Point {
    double x, y;
};

template<>
struct std::formatter<Point> {
    char presentation = 'f';
    int precision = 2;

    auto parse(format_parse_context& ctx) {
        auto it = ctx.begin();
        if (it != ctx.end() && *it == 'f') {
            presentation = 'f';
            ++it;
        }
        if (it != ctx.end() && *it == ':') {
            ++it;
            auto [end, ec] = std::from_chars(it, ctx.end(), precision);
            if (ec == std::errc()) it = end;
        }
        return it;
    }

    auto format(const Point& p, format_context& ctx) const {
        return std::format_to(ctx.out(), "({:.{}}, {:.{}})", p.x, precision, p.y, precision);
    }
};

int main() {
    Point p{3.14159, 2.71828};
    std::cout << std::format("{}\n", p);       // (3.14, 2.72)
    std::cout << std::format("{:f:4}\n", p);   // (3.1416, 2.7183)
    return 0;
}
Output
(3.14, 2.72)
(3.1416, 2.7183)
⚠ Complex Parsing
📊 Production Insight
Custom formatters are essential for logging complex objects. Keep them simple to avoid performance hits.
🎯 Key Takeaway
Specialize std::formatter to enable std::format for your types.

Performance and std::format_to

std::format returns a std::string, which involves allocation. For performance-critical code, use std::format_to which writes to an output iterator. You can write to a pre-allocated buffer, a file, or a custom sink. std::format_to returns an iterator past the last written character. Also consider using std::formatted_size to compute the required buffer size without formatting.

Example: Write formatted output to a fixed-size buffer.

format_to.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <format>
#include <iostream>
#include <array>

int main() {
    std::array<char, 64> buffer;
    auto it = std::format_to(buffer.begin(), "Value: {:.2f}", 3.14159);
    *it = '\0'; // null-terminate
    std::cout << buffer.data() << '\n';

    // Using formatted_size
    auto size = std::formatted_size("Hello, {}!", "world");
    std::cout << "Size: " << size << '\n';

    return 0;
}
Output
Value: 3.14
Size: 13
🔥std::print in C++23
📊 Production Insight
In high-frequency logging, use std::format_to with a pre-allocated ring buffer to avoid heap allocations.
🎯 Key Takeaway
Use std::format_to for zero-allocation formatting in hot paths.

Locale and Unicode Support

std::format respects the global locale for formatting numbers (e.g., thousands separator). Use the 'L' specifier to enable locale-specific formatting. For example, {:L} formats an integer with locale-specific digit grouping. Unicode support is limited; std::format works with char and wchar_t strings. For UTF-8, use std::string (char8_t in C++20). However, width and alignment may not correctly handle multi-byte characters. Consider using libraries like fmt for full Unicode support.

Example: Locale-aware formatting.

locale_format.cppCPP
1
2
3
4
5
6
7
8
9
10
11
#include <format>
#include <iostream>
#include <locale>

int main() {
    std::locale::global(std::locale("en_US.UTF-8"));
    int big = 1234567;
    std::cout << std::format("Default: {}\n", big);
    std::cout << std::format("Locale: {:L}\n", big);
    return 0;
}
Output
Default: 1234567
Locale: 1,234,567
⚠ Locale Dependency
📊 Production Insight
Avoid locale-dependent formatting in performance-critical code; pre-format with fixed separators instead.
🎯 Key Takeaway
Use the 'L' specifier for locale-aware formatting.

Error Handling and Debugging

std::format throws std::format_error on invalid format strings (e.g., mismatched braces, invalid specifiers). Always catch exceptions in production code. Use try-catch blocks around format calls. For debugging, you can use std::vformat with a std::format_args to inspect arguments. Also, consider using compile-time checks with constexpr format strings (C++20 allows constexpr std::format).

Example: Catching format errors.

error_handling.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <format>
#include <iostream>

int main() {
    try {
        std::string s = std::format("Missing brace: {0:", 42);
    } catch (const std::format_error& e) {
        std::cerr << "Format error: " << e.what() << '\n';
    }

    // Compile-time check (C++20)
    constexpr auto msg = std::format("Hello {}!", "world");
    std::cout << msg << '\n';

    return 0;
}
Output
Format error: missing '}'
Hello world!
💡Compile-Time Validation
📊 Production Insight
Wrap format calls in a logging macro that catches exceptions to prevent crashes.
🎯 Key Takeaway
Always handle std::format_error in production code.
● Production incidentPOST-MORTEMseverity: high

The Logging Outage: How printf Crashed a Production Server

Symptom
Server crashed intermittently with segfaults; logs were garbled.
Assumption
Developers assumed a memory leak or hardware issue.
Root cause
A printf call with a %s format but an integer argument caused undefined behavior, corrupting memory.
Fix
Replaced all printf calls with std::format, which catches type mismatches at compile time.
Key lesson
  • Always use type-safe formatting functions.
  • Avoid printf in modern C++ code.
  • Enable compiler warnings for format string mismatches.
  • Use static analysis tools to detect unsafe calls.
  • Prefer std::format for new code.
Production debug guideSymptom to Action4 entries
Symptom · 01
std::format throws std::format_error
Fix
Check format string syntax; ensure braces are balanced.
Symptom · 02
Output has wrong width or precision
Fix
Verify format specifiers; use {:>10} for right-align.
Symptom · 03
Custom type not formatting
Fix
Implement std::formatter specialization for the type.
Symptom · 04
Performance degradation with many formats
Fix
Use std::format_to or pre-compiled format strings.
★ Quick Debug Cheat SheetCommon format errors and fixes
std::format_error: 'missing '}'
Immediate action
Escape braces: use {{ for literal {.
Commands
std::format("{{hello}}")
std::format("{{}}", 42)
Fix now
Replace { with {{ and } with }}.
Wrong type error+
Immediate action
Check argument types match placeholders.
Commands
std::format("{}", 42)
std::format("{:d}", 42)
Fix now
Use correct format specifier (e.g., :d for int).
Custom type not printing+
Immediate action
Add formatter specialization.
Commands
template<> struct std::formatter<MyType> { ... };
std::format("{}", myObj)
Fix now
Implement parse and format methods.
Featureprintfiostreamsstd::format
Type safetyNoYesYes
PerformanceFastSlowFast
Custom typesNoYes (operator<<)Yes (formatter)
Locale supportPartialYesYes (with L specifier)
Compile-time checksNoNoYes (constexpr)
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
basic_format.cppint main() {Basic Usage of std
format_specifiers.cppint main() {Format Specifiers in Depth
custom_formatter.cppstruct Point {Formatting User-Defined Types
format_to.cppint main() {Performance and std
locale_format.cppint main() {Locale and Unicode Support
error_handling.cppint main() {Error Handling and Debugging

Key takeaways

1
std::format provides type-safe, expressive string formatting.
2
Use format specifiers for precise control over output.
3
Custom formatters enable formatting of user-defined types.
4
Prefer std::format_to for performance-critical code.
5
Always handle std::format_error exceptions in production.

Common mistakes to avoid

3 patterns
×

Forgetting to escape braces: using { instead of {{ for literal brace.

×

Mismatching argument types: e.g., using {:d} with a string.

×

Assuming std::format modifies the arguments.

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the syntax of format specifiers in std::format.
Q02SENIOR
How would you implement a custom formatter for a class representing a co...
Q03SENIOR
What are the performance implications of using std::format vs std::ostri...
Q01 of 03JUNIOR

Explain the syntax of format specifiers in std::format.

ANSWER
The syntax is [[fill]align][sign][#][0][width][.precision][type]. Fill is a character, align is <, >, or ^. Sign is +, -, or space. # enables alternate form. 0 pads with zeros. Width is minimum field width. Precision is number of digits or max string length. Type is d, x, f, etc.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between std::format and printf?
02
Can I use std::format with wchar_t?
03
How do I format a std::chrono time point?
04
Is std::format available in C++17?
05
How do I format a string with leading zeros?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's C++ Advanced. Mark it forged?

3 min read · try the examples if you haven't

Previous
C++20 Modules Complete Guide
23 / 41 · C++ Advanced
Next
C++23 std::expected and std::optional Monadic Operations