Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 14 – Input/output, files, formatting and time

std::format and std::print in depth

Every option of the C++ format specification, why {} prints 100000.0 as 1e+05, formatting into buffers with format_to_n, and run-time format strings.

  • Intermediate
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Write format specifications with fill, alignment, sign,
  • Predict what {} and the floating-point presentation types print for a double
  • Format into existing buffers with std::format_to, std::format_to_n and std::formatted_size
  • Use std::vformat for format strings known only at run time, and handle std::format_error

Before you start

What you’ll learn · Before you start

What you will learn

  • Write format specifications with fill, alignment, sign,
  • Predict what {} and the floating-point presentation types print for a double
  • Format into existing buffers with std::format_to, std::format_to_n and std::formatted_size
  • Use std::vformat for format strings known only at run time, and handle std::format_error

Before you start

On this page

std::format turns a format string and some values into a std::string, and C++23’s std::print and std::println write the same text straight to the console or a file; std::println also ends the line. Each {} in the format string is a replacement field. What follows a colon inside it, such as >10.2f, is the field’s format specification, and its parts always come in the same order: a fill character and an alignment, a sign, #, 0, a width, a precision, L and a presentation type. The compiler checks every format string written in the code, so an unknown option or a missing argument stops the build. To write into memory you already have, std::format_to and std::format_to_n skip the temporary string, and a format string that only arrives while the program runs goes through std::vformat, which checks it then.

Every example below is built and run with Apple clang 21 and with GCC 16, and the two outputs are shown together.

The format specification, option by option

Every part of a specification can be left out, but the parts that are present must keep this order:

Part Written as What it does
Fill and alignment *^, >, < < puts the value on the left of the field, > on the right and ^ in the centre; a character before it, any but a brace, is the fill
Sign +, -, a space + signs positive numbers too, a space leaves room for a sign, - (the default) signs only negative ones
# #x, #.0f The alternate form: a 0x, 0b or 0 prefix for integers, a decimal point that always stays for floating-point numbers
0 06 Pads with zeros after the sign and the prefix, but only when the specification has no alignment
Width 10, {} The smallest number of columns the field takes; {} reads the width from the next argument
Precision .2, .{} Digits after the point for f and e, significant digits for g, and the most columns a string may take
L L Uses the locale for the decimal point and digit grouping
Type d x b o e f g a c s p ? How the value is written: the base of an integer, the notation of a floating-point number, ? for escaped text

Each line of this program shows one specification and what it printed between brackets, and the table at the end writes four numbers in four bases:

spec_fields.cpp C++ · spec_fields.cpp
// The options of a format specification one at a time, then a table of numbers in four bases.
#include <print>

int main() {
    // {{ and }} print a brace, so each line shows its placeholder and then what it printed
    std::println("{{:*^9}}    [{:*^9}]", "mid");               // centre in 9 columns, pad with *
    std::println("{{:>6}}     [{:>6}]", 42);                   // right-align in 6 columns
    std::println("{{:<6}}     [{:<6}]", 42);                   // left-align
    std::println("{{:+}}      [{:+}] [{:+}]", 7, -7);           // a sign for positive numbers too
    std::println("{{: }}      [{: }] [{: }]", 7, -7);           // a space where the plus would go
    std::println("{{:06}}     [{:06}] [{:06}]", 42, -42);       // zeros after the sign, up to the width
    std::println("{{:<06}}    [{:<06}]", 42);                  // with an alignment, the 0 does nothing
    std::println("{{:#x}}     [{:#x}] [{:#X}] [{:#o}]", 255, 255, 8);
    std::println("{{:#010b}}  [{:#010b}]", 5);                 // the 0b prefix counts towards the 10
    std::println("{{:.3}}     [{:.3}]", "abcdef");             // a precision cuts a string to 3 columns
    std::println("{{:>9.2f}}  [{:>9.2f}]", 3.14159);
    std::println("");

    std::println("{:>8} {:>8} {:>8} {:>18} {:>11}", "decimal", "hex", "octal", "binary", "scientific");
    for (int v : {42, 255, -1024, 65535}) {
        // {0} and {1} pick arguments by position, so one value fills several columns
        std::println("{0:>8} {0:>#8x} {0:>#8o} {0:>#18b} {1:>11.3e}", v, static_cast<double>(v));
    }
}
Each option on its own, then numbers in four bases Python · spec_session.py
from session import run

# spec_fields.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra spec_fields.cpp -o spec_fields && ./spec_fields
g++-16 -std=c++23 -Wall -Wextra spec_fields.cpp -o spec_fields-gcc && ./spec_fields-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra spec_fields.cpp -o spec_fields && ./spec_fields
{:*^9}    [***mid***]
{:>6}     [    42]
{:<6}     [42    ]
{:+}      [+7] [-7]
{: }      [ 7] [-7]
{:06}     [000042] [-00042]
{:<06}    [42    ]
{:#x}     [0xff] [0XFF] [010]
{:#010b}  [0b00000101]
{:.3}     [abc]
{:>9.2f}  [     3.14]

 decimal      hex    octal             binary  scientific
      42     0x2a      052           0b101010   4.200e+01
     255     0xff     0377         0b11111111   2.550e+02
   -1024   -0x400   -02000     -0b10000000000  -1.024e+03
   65535   0xffff  0177777 0b1111111111111111   6.554e+04
$ g++-16 -std=c++23 -Wall -Wextra spec_fields.cpp -o spec_fields-gcc && ./spec_fields-gcc
{:*^9}    [***mid***]
{:>6}     [    42]
{:<6}     [42    ]
{:+}      [+7] [-7]
{: }      [ 7] [-7]
{:06}     [000042] [-00042]
{:<06}    [42    ]
{:#x}     [0xff] [0XFF] [010]
{:#010b}  [0b00000101]
{:.3}     [abc]
{:>9.2f}  [     3.14]

 decimal      hex    octal             binary  scientific
      42     0x2a      052           0b101010   4.200e+01
     255     0xff     0377         0b11111111   2.550e+02
   -1024   -0x400   -02000     -0b10000000000  -1.024e+03
   65535   0xffff  0177777 0b1111111111111111   6.554e+04

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 spec_session.py

The two compilers agree on every character. A few results are worth reading twice:

  • Without an alignment, numbers sit on the right of their field and text on the left; {:>6} and {:<6} choose.
  • {:06} puts the zeros after the sign, so -42 becomes -00042. {:<06} prints 42 and four spaces: once an alignment is given, the 0 option does nothing.
  • The prefix counts towards the width: {:#010b} gives 0b00000101, ten characters in all.
  • A negative number keeps its minus sign in every base, so -1024 is -0x400. {:x} of -1 prints -1, not the ffffffff that printf("%x") gives; convert to an unsigned type to see the bits.
  • {0} and {1} choose arguments by position, which lets one value fill four columns. Numbering is all or nothing: a string that mixes {} with {0} does not compile with either compiler.
  • {{ and }} print a single brace, which is how each line printed its own specification.

The options act in a fixed order, which explains every result above:

std::format makes the digits from the type and precision, adds the sign and prefix, then pads to the width or with zeros.std::format("{:>+10.2f}", 3.14159)1. Type and precisionf with .2 rounds to "3.14"2. Sign and #+ writes "+3.14" (# would add0x or 0b, or keep a point)3. Width, fill and alignment> pads on the left to 10columns: 5 spaces, then +3.143. instead, with 0 and no alignment:{:+010.2f} puts zeros afterthe sign: "+000003.14"or

The order in which a format specification acts

Text description of the diagram

The diagram follows std::format("{:>+10.2f}", 3.14159) through the three steps that every replacement field goes through.

  1. The type and the precision make the digits: the type f with the precision .2 rounds 3.14159 to "3.14".
  2. The sign and the # option add what goes in front of the digits: + writes "+3.14". For integers, # would add a prefix such as 0x or 0b; for floating-point types it keeps the decimal point.
  3. The width, the fill character and the alignment pad the whole result: > pads on the left with the fill, a space by default, until the field is 10 columns wide, giving five spaces followed by +3.14.
  4. The other branch of step 3: with the 0 option and no alignment, as in {:+010.2f}, zeros go between the sign and the digits instead, giving "+000003.14". With an alignment, the 0 option does nothing.

What {} prints for floating-point numbers

Without a type, a double is written in the shortest form that reads back as exactly the same value: the rule that the standard takes from std::to_chars. The same rule chooses between fixed and exponent notation by length alone, and that surprises almost everyone:

floats.cpp C++ · floats.cpp
// What {} prints for a double, and what a type and a precision change.
#include <limits>
#include <print>

int main() {
    // {} prints the shortest text that reads back as exactly the same double ...
    std::println("[{}] [{}] [{}]", 0.1, 0.1 + 0.2, 1.0 / 3);
    // ... and uses exponent form whenever that text is shorter
    std::println("[{}] [{}] [{}] [{}]", 100000.0, 120000.0, 1000000.0, 0.0001);
    // a type and a precision decide instead
    std::println("[{:f}] [{:.2f}] [{:.3e}] [{:g}] [{:.3g}]", 100000.0, 100000.0, 100000.0, 100000.0, 100000.0);
    // rounding works on the stored binary value, which is a little below 2.675
    std::println("[{:.2f}] [{:.20f}]", 2.675, 2.675);
    // # keeps the decimal point; zero has a sign; infinity and NaN have names
    std::println("[{:#.0f}] [{:.0f}] [{}] [{}] [{}]", 2.0, 2.0, -0.0,
                 std::numeric_limits<double>::infinity(), std::numeric_limits<double>::quiet_NaN());
    // a float is printed with the digits a float needs, not a double's
    std::println("[{}] [{}]", 0.1f, static_cast<double>(0.1f));
}
Shortest output, types and precisions Python · floats_session.py
from session import run

# floats.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra floats.cpp -o floats && ./floats
g++-16 -std=c++23 -Wall -Wextra floats.cpp -o floats-gcc && ./floats-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra floats.cpp -o floats && ./floats
[0.1] [0.30000000000000004] [0.3333333333333333]
[1e+05] [120000] [1e+06] [1e-04]
[100000.000000] [100000.00] [1.000e+05] [100000] [1e+05]
[2.67] [2.67499999999999982236]
[2.] [2] [-0] [inf] [nan]
[0.1] [0.10000000149011612]
$ g++-16 -std=c++23 -Wall -Wextra floats.cpp -o floats-gcc && ./floats-gcc
[0.1] [0.30000000000000004] [0.3333333333333333]
[1e+05] [120000] [1e+06] [1e-04]
[100000.000000] [100000.00] [1.000e+05] [100000] [1e+05]
[2.67] [2.67499999999999982236]
[2.] [2] [-0] [inf] [nan]
[0.1] [0.10000000149011612]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 floats_session.py

0.1 prints as 0.1 although the stored value is slightly more, because no shorter text reads back as that double; 0.1 + 0.2 needs all 17 digits of 0.30000000000000004. In the second line, 100000.0 became 1e+05, five characters against six for 100000, and 1000000.0 and 0.0001 went the same way, while 120000.0 stayed 120000 because its exponent form is no shorter. A report or an export that must show the digits should say so with a type: {:.0f} or {:.2f}. Paper P3505 proposes letting the size of the number decide instead, so that 100000.0 would print as 100000; the committee’s library group sent it on for a standard after C++26.

The other lines show what a type and a precision change:

  • f and e take the precision as digits after the point (six by default), and g as significant digits, dropping zeros at the end.
  • {:.2f} rounds the value that is stored, and 2.675 is stored as 2.67499999999999982236…, so it prints 2.67, as printf does. Amounts of money are safer as whole numbers of the smallest unit, formatted from those integers, as the exercise below does.
  • # keeps the decimal point (2.), -0.0 keeps its sign, and infinity and NaN print as inf and nan.
  • A float gets the digits a float needs: 0.1f prints as 0.1, and the same value converted to double prints as 0.10000000149011612.
IEEE 754 Floating-Point Converter See the exact binary value of 0.1 or 2.675 and why its decimal digits never end.

Text, Unicode widths and escaped output

For text, the width and the precision count columns on the screen, not bytes. The standard asks for an estimate: it treats each extended grapheme cluster, the unit Unicode calls a user-perceived character, as one column, and wide East Asian characters and most emoji as two:

text.cpp C++ · text.cpp
// Strings and other values: what a width counts, escaped output, and ranges.
#include <format>
#include <map>
#include <print>
#include <string>
#include <string_view>
#include <vector>

int main() {
    // A width counts columns on the screen, not bytes.
    for (std::string_view s : {"abc", "€20", "₹500", "नमस्ते", "😀"}) {
        std::println("[{:>8}] is {:>2} bytes, {:>2} bytes padded", s, s.size(), std::formatted_size("{:>8}", s));
    }
    // ? prints a string or a character the way you would write it in code.
    std::println("[{}] [{:?}] [{:?}]", "tab\tand\nnewline", "tab\tand\nnewline", '\'');
    // bool, char, pointers
    std::println("[{}] [{:d}] [{}] [{:d}] [{:c}] [{}]", true, true, 'A', 'A', 66, nullptr);
    // C++23 formats ranges and maps, with a specification for each element after a second colon.
    std::vector<int> codes{10, 255, 7};
    std::map<std::string, int> stock{{"pens", 40}, {"ink", 3}};
    std::vector<std::string> names{"Asha Rao", "Ravi"};
    std::println("[{}] [{:n}] [{::#x}] [{}] [{}]", codes, codes, codes, stock, names);
}
Widths, escaped strings, other types and ranges Python · text_session.py
from session import run

# text.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra text.cpp -o text && ./text
g++-16 -std=c++23 -Wall -Wextra text.cpp -o text-gcc && ./text-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra text.cpp -o text && ./text
[     abc] is  3 bytes,  8 bytes padded
[     €20] is  5 bytes, 10 bytes padded
[    ₹500] is  6 bytes, 10 bytes padded
[     नमस्ते] is 18 bytes, 23 bytes padded
[      😀] is  4 bytes, 10 bytes padded
[tab	and
newline] ["tab\tand\nnewline"] ['\'']
[true] [1] [A] [65] [B] [0x0]
[[10, 255, 7]] [10, 255, 7] [[0xa, 0xff, 0x7]] [{"ink": 3, "pens": 40}] [["Asha Rao", "Ravi"]]
$ g++-16 -std=c++23 -Wall -Wextra text.cpp -o text-gcc && ./text-gcc
[     abc] is  3 bytes,  8 bytes padded
[     €20] is  5 bytes, 10 bytes padded
[    ₹500] is  6 bytes, 10 bytes padded
[     नमस्ते] is 18 bytes, 23 bytes padded
[      😀] is  4 bytes, 10 bytes padded
[tab	and
newline] ["tab\tand\nnewline"] ['\'']
[true] [1] [A] [65] [B] [0x0]
[[10, 255, 7]] [10, 255, 7] [[0xa, 0xff, 0x7]] [{"ink": 3, "pens": 40}] [["Asha Rao", "Ravi"]]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 text_session.py

"₹500" is 6 bytes in UTF-8 but 4 columns wide, so {:>8} added four spaces, and std::formatted_size reported the 10 bytes of the result. The Hindi word नमस्ते is 18 bytes and, by both libraries’ estimate, 3 columns: the conjunct स्ते at its end, four code points, forms one cluster. The estimate is only an estimate, though, because terminals and fonts draw scripts such as Devanagari in different widths, so columns of such text may still not line up on every screen. The emoji counts as two columns.

The rest of the output covers the other types:

  • {:?} writes a string or a character the way you would write it in code: quoted, with \t, \n and quote marks escaped. It is the right choice for log lines, where a hidden tab or line break would otherwise vanish.
  • bool prints as true, or as 1 with {:d}; a char prints as a character, or as its code with {:d}, and an integer prints as a character with {:c}. Pointers print in hexadecimal, and nullptr as 0x0.
  • C++23 formats ranges: a vector prints with brackets and commas, {:n} drops the brackets, and a specification after a second colon applies to every element, so {::#x} writes each code in hexadecimal. Maps print as {key: value} pairs, and strings inside a range are quoted and escaped.
Unicode Character Inspector Look up the code points and byte lengths of any text before you decide on a width.

Writing into buffers you already have

std::format returns a new string every time. Three functions let you decide where the characters go:

buffers.cpp C++ · buffers.cpp
// Formatting into memory you already have: format_to_n, formatted_size and format_to.
#include <cstddef>
#include <cstdlib>
#include <format>
#include <iterator>
#include <new>
#include <print>
#include <string>
#include <string_view>

// Every allocation goes through operator new, so counting here shows which calls allocate.
static long allocations = 0;
void* operator new(std::size_t n) {
    ++allocations;
    if (void* p = std::malloc(n ? n : 1)) return p;
    throw std::bad_alloc{};
}
void operator delete(void* p) noexcept { std::free(p); }
void operator delete(void* p, std::size_t) noexcept { std::free(p); }

int main() {
    // 1. A fixed buffer: format_to_n writes at most n characters and reports how many the whole result has.
    char buf[12];
    auto r = std::format_to_n(buf, sizeof buf, "Total: {:.2f}", 1234567.891);
    std::string_view written(buf, static_cast<std::size_t>(r.out - buf));
    std::println("wrote [{}]: {} of {} characters{}", written, written.size(), r.size,
                 r.size > std::ssize(buf) ? ", the rest was cut" : "");

    // 2. formatted_size: the room a result needs, before writing it.
    std::println("formatted_size: {}", std::formatted_size("Total: {:.2f}", 1234567.891));

    // 3. One string reused for every row: no allocation once it has the capacity.
    std::string row;
    row.reserve(64);
    long before = allocations;
    for (int i = 0; i < 1000; ++i) {
        row.clear();
        std::format_to(std::back_inserter(row), "row {:>4} {:>10.2f} and a longer label", i, i * 1.5);
    }
    long reused = allocations - before;

    // 4. A new string for every row: one allocation each, once a row is too long for the string itself.
    before = allocations;
    std::string made;
    for (int i = 0; i < 1000; ++i) made = std::format("row {:>4} {:>10.2f} and a longer label", i, i * 1.5);
    long fresh = allocations - before;

    std::println("1000 rows: format_to into one string allocated {} times, std::format {} times", reused, fresh);
    std::println("a std::string holds up to {} characters without allocating", std::string().capacity());
    std::println("last row: [{}]", row);
}
A fixed buffer, the size needed, and a counter of allocations Python · buffers_session.py
from session import run

# buffers.cpp built without optimisation (so the allocation counter sees every call) and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra buffers.cpp -o buffers && ./buffers
g++-16 -std=c++23 -Wall -Wextra buffers.cpp -o buffers-gcc && ./buffers-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra buffers.cpp -o buffers && ./buffers
wrote [Total: 12345]: 12 of 17 characters, the rest was cut
formatted_size: 17
1000 rows: format_to into one string allocated 0 times, std::format 1000 times
a std::string holds up to 22 characters without allocating
last row: [row  999    1498.50 and a longer label]
$ g++-16 -std=c++23 -Wall -Wextra buffers.cpp -o buffers-gcc && ./buffers-gcc
wrote [Total: 12345]: 12 of 17 characters, the rest was cut
formatted_size: 17
1000 rows: format_to into one string allocated 0 times, std::format 1000 times
a std::string holds up to 15 characters without allocating
last row: [row  999    1498.50 and a longer label]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 buffers_session.py

  • std::format_to_n(out, n, …) writes at most n characters and returns a pair: out, where the writing stopped, and size, the length the whole result has. Here 12 characters went into the 12-character buffer, and size was 17, so the caller knows the end was cut. It never writes a terminating '\0'; add one yourself when a C function will read the buffer.
  • std::formatted_size(…) returns that length without storing anything, so you can check that a result fits before you write it.
  • std::format_to(out, …) writes through any output iterator. With std::back_inserter and one string that is cleared and reused, a thousand rows caused no allocation at all, while std::format allocated once per row on both libraries. Each row was too long to fit inside the string object itself, which holds 22 characters with libc++ and 15 with libstdc++ before it needs memory of its own.

Allocations rarely matter for a few lines of output. They matter in a logger that formats hundreds of thousands of lines a second, in a game’s frame loop, and in embedded code that may not use the heap at all.

std::print and std::println: where the text goes

std::print(fmt, args…) writes to standard output, and std::println adds a line break. Both take the same format strings as std::format, and both have overloads for other destinations:

  • std::print(stderr, …) writes to any C FILE*, which is how error messages reach standard error.
  • std::print(std::cout, …), declared in <ostream>, writes to any output stream, such as a file opened with std::ofstream.
  • In C++26, std::println() with no arguments prints an empty line; both compilers accept it with -std=c++26.

When the output is a terminal and the program’s text is UTF-8, std::print writes through the system’s Unicode interface, which is what makes such text appear correctly in a Windows console; the lesson on console input and output shows the build flags for that.

Format strings known only at run time

std::format("{} of {}", a, b) compiles only because the compiler can read the format string: a constructor that runs while compiling checks it against the types of the arguments. A string read from a file, a translation catalogue or a database is not available then, so it goes to std::vformat, with the arguments packed by std::make_format_args:

runtime.cpp C++ · runtime.cpp
// Format strings that exist only while the program runs: std::vformat and std::make_format_args.
#include <format>
#include <print>
#include <string>
#include <string_view>

// make_format_args stores references to its arguments, so they must be variables that outlive the call.
std::string fill_in(std::string_view templ, int count, std::string_view item) {
    return std::vformat(templ, std::make_format_args(count, item));
}

int main() {
    std::println("{}", fill_in("{} x {}", 3, "pen"));
    std::println("{}", fill_in("{1}: {0:>4}", 12, "ink"));
    for (std::string_view bad : {"{} x {} x {}", "{:q}", "{"}) {
        try {
            std::println("{}", fill_in(bad, 1, "x"));
        } catch (const std::format_error& e) {
            std::println("{:<12} -> std::format_error: {}", bad, e.what());
        }
    }
    // A width or precision chosen at run time needs no vformat: a nested {} takes it from the arguments.
    int width = 9;
    int digits = 2;
    std::println("[{:>{}}] [{:^{}.{}f}]", "right", width, 3.14159, width, digits);
}
dynamic.cpp C++ · dynamic.cpp
// C++26: mark a format string as known only at run time and pass it to std::println directly.
#include <format>
#include <print>
#include <string>

int main() {
    std::string templ = "{} of {} files copied";
#if __cpp_lib_format >= 202603L
    std::println(std::dynamic_format(templ), 3, 5);   // the name in C++26 (GCC 16)
#else
    std::println(std::runtime_format(templ), 3, 5);   // the earlier name (Apple clang 21)
#endif
    std::println("__cpp_lib_format is {}", __cpp_lib_format);
}
Run-time format strings in C++23, then with the C++26 marker Python · runtime_session.py
from session import run

# runtime.cpp with both compilers in C++23 mode, then dynamic.cpp in C++26 mode.
run("""
clang++ -std=c++23 -Wall -Wextra runtime.cpp -o runtime && ./runtime
g++-16 -std=c++23 -Wall -Wextra runtime.cpp -o runtime-gcc && ./runtime-gcc
clang++ -std=c++26 -Wall -Wextra dynamic.cpp -o dynamic && ./dynamic
g++-16 -std=c++26 -Wall -Wextra dynamic.cpp -o dynamic-gcc && ./dynamic-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra runtime.cpp -o runtime && ./runtime
3 x pen
ink:   12
{} x {} x {} -> std::format_error: The argument index value is too large for the number of arguments supplied
{:q}         -> std::format_error: The format specifier should consume the input or end with a '}'
{            -> std::format_error: The format string terminates at a '{'
[    right] [  3.14   ]
$ g++-16 -std=c++23 -Wall -Wextra runtime.cpp -o runtime-gcc && ./runtime-gcc
3 x pen
ink:   12
{} x {} x {} -> std::format_error: format error: invalid arg-id in format string
{:q}         -> std::format_error: format error: failed to parse format-spec
{            -> std::format_error: format error: unmatched '{' in format string
[    right] [  3.14   ]
$ clang++ -std=c++26 -Wall -Wextra dynamic.cpp -o dynamic && ./dynamic
3 of 5 files copied
__cpp_lib_format is 202110
$ g++-16 -std=c++26 -Wall -Wextra dynamic.cpp -o dynamic-gcc && ./dynamic-gcc
3 of 5 files copied
__cpp_lib_format is 202603

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 runtime_session.py

Three things show in the output:

  1. A bad format string is reported when it is used, as std::format_error. The two libraries word their messages differently, so code should catch the type and never test the text.
  2. std::make_format_args keeps references to its arguments, so they must be variables that live until the call returns. Passing a temporary, such as a std::string built in the call, does not compile with either library: paper P2905 made that a compile error because the reference would dangle.
  3. A width or precision chosen at run time needs no vformat. A nested {} takes it from the next argument, as the last line did with {:>{}} and {:^{}.{}f}, and the format string stays checked while compiling.

C++26: dynamic_format, formerly runtime_format

C++26 lets a run-time string go straight to std::format and std::println once it is marked. Paper P2918 named the marker std::runtime_format, and paper P3953, adopted for C++26, renamed it std::dynamic_format, because a format string can now also be formatted while compiling. GCC 16 has the new name and reports __cpp_lib_format as 202603; the libc++ of Apple clang 21 still has only std::runtime_format, which is why dynamic.cpp tests the macro.

A format string written by a user deserves the care of any other input. Being valid does not make it harmless: {:>1000000000} is a correct specification, and filling it in builds a gigabyte of spaces. Check a template when it is saved, allow only the fields and the widths you expect, and catch std::format_error wherever templates are filled in.

Your turn: a bank statement printer

statement.h declares three functions for printing a bank statement; amounts are kept in paise, 100 to the rupee, so no floating-point rounding can creep in. Complete them:

  1. amount(paise) writes rupees, a point and exactly two digits of paise: 123456 gives 1234.56, 1005 gives 10.05, and -50 gives -0.50.
  2. statement_line(entry, balance) writes the date, two spaces, the description left-aligned in 20 columns and cut if it is longer, two spaces, the amount right-aligned in 10 columns, two spaces and the balance right-aligned in 12.
  3. closing_line(balance) writes Closing balance left-aligned in 34 columns and the balance right-aligned in 22.
The starter code and its sample tests

C header · statement.h

// A bank statement printer. Complete amount(), statement_line() and closing_line(); the tests are in statement_tests.cpp.
#pragma once
#include <format>
#include <string>
#include <string_view>

struct Entry {
    std::string_view date;          // for example "2025-04-03"
    std::string_view description;   // shown in at most 20 columns
    long long paise;                // 100 paise make a rupee; money in is positive, money out negative
};

// The amount in rupees with exactly two decimals: 123456 -> "1234.56", -50 -> "-0.50".
inline std::string amount(long long paise) {
    return std::to_string(paise);   // TODO: rupees, a point and two digits of paise
}

// One row: the date, two spaces, the description left-aligned in 20 columns (cut if longer), two spaces,
// the amount right-aligned in 10 columns, two spaces and the balance after this entry right-aligned in 12.
// Example: "2025-04-03  Rent                   -15000.00       9500.00"
inline std::string statement_line(const Entry& e, long long balance_paise) {
    return std::format("{} {} {} {}", e.date, e.description, amount(e.paise), amount(balance_paise));   // TODO
}

// The last line: "Closing balance" left-aligned in 34 columns, then the balance right-aligned in 22.
inline std::string closing_line(long long balance_paise) {
    return std::format("Closing balance {}", amount(balance_paise));   // TODO
}

C++ · statement_tests.cpp

// Sample tests for statement.h: each check compares the exact text, as a judge compares output.
#include <print>
#include <string>
#include <string_view>
#include "statement.h"

int passed = 0, total = 0;

void check(std::string_view what, const std::string& got, std::string_view want) {
    ++total;
    if (got == want) ++passed;
    else std::println("FAIL {}\n  got  [{}]\n  want [{}]", what, got, want);
}

int main() {
    check("amount with paise", amount(123456), "1234.56");
    check("amount under one rupee, negative", amount(-50), "-0.50");
    check("amount with a zero digit of paise", amount(1005), "10.05");
    check("a row", statement_line({"2025-04-03", "Rent", -1500000}, 950000),
          "2025-04-03  Rent                   -15000.00       9500.00");
    check("a long description is cut to 20 columns",
          statement_line({"2025-04-05", "Electricity bill for March", -184250}, 765750),
          "2025-04-05  Electricity bill for    -1842.50       7657.50");
    check("a description with an accent takes its columns, not its bytes",
          statement_line({"2025-04-09", "Café Mocha", -32000}, 733750),
          "2025-04-09  Café Mocha               -320.00       7337.50");
    check("the closing line is 56 columns wide",
          closing_line(733750), "Closing balance                                  7337.50");
    std::println("{} of {} checks passed", passed, total);
    return passed == total ? 0 : 1;
}

The tests compare every character, the way an online judge compares output. Against the starter code, all seven fail:

The sample tests against the starter code Python · statement_session.py
from session import run

# The sample tests against the starter code, with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra statement_tests.cpp -o statement_tests && ./statement_tests
g++-16 -std=c++23 -Wall -Wextra statement_tests.cpp -o statement_tests-gcc && ./statement_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra statement_tests.cpp -o statement_tests && ./statement_tests
FAIL amount with paise
  got  [123456]
  want [1234.56]
FAIL amount under one rupee, negative
  got  [-50]
  want [-0.50]
FAIL amount with a zero digit of paise
  got  [1005]
  want [10.05]
FAIL a row
  got  [2025-04-03 Rent -1500000 950000]
  want [2025-04-03  Rent                   -15000.00       9500.00]
FAIL a long description is cut to 20 columns
  got  [2025-04-05 Electricity bill for March -184250 765750]
  want [2025-04-05  Electricity bill for    -1842.50       7657.50]
FAIL a description with an accent takes its columns, not its bytes
  got  [2025-04-09 Café Mocha -32000 733750]
  want [2025-04-09  Café Mocha               -320.00       7337.50]
FAIL the closing line is 56 columns wide
  got  [Closing balance 733750]
  want [Closing balance                                  7337.50]
0 of 7 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra statement_tests.cpp -o statement_tests-gcc && ./statement_tests-gcc
FAIL amount with paise
  got  [123456]
  want [1234.56]
FAIL amount under one rupee, negative
  got  [-50]
  want [-0.50]
FAIL amount with a zero digit of paise
  got  [1005]
  want [10.05]
FAIL a row
  got  [2025-04-03 Rent -1500000 950000]
  want [2025-04-03  Rent                   -15000.00       9500.00]
FAIL a long description is cut to 20 columns
  got  [2025-04-05 Electricity bill for March -184250 765750]
  want [2025-04-05  Electricity bill for    -1842.50       7657.50]
FAIL a description with an accent takes its columns, not its bytes
  got  [2025-04-09 Café Mocha -32000 733750]
  want [2025-04-09  Café Mocha               -320.00       7337.50]
FAIL the closing line is 56 columns wide
  got  [Closing balance 733750]
  want [Closing balance                                  7337.50]
0 of 7 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 statement_session.py

Hint: build the amount’s text first, with the sign written by hand and the paise as {:02}, then give that finished text its width in statement_line(); a precision on the description’s field cuts it to 20 columns. When both compilers print 7 of 7 checks passed, add a line with a description in Devanagari or with an emoji to the tests and predict its padding before you run them.

Text Diff Paste your output and the expected line to see the exact character where they differ.

Check yourself

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does std::format("{:<06}", 42) return?

    Choose one answer.

    Show the answer to question 1

    Answer: 42 followed by four spaces: the alignment wins and the 0 does nothing

    The 0 option pads with zeros only when the specification has no alignment. With < the field is padded with the fill character, a space by default, after the number.

  2. Question 2 of 6 What does std::println("{}", 100000.0) print with both libraries?

    Choose one answer.

    Show the answer to question 2

    Answer: 1e+05

    Without a type, a double is printed in its shortest form that reads back as the same value, and when the exponent form is shorter it wins: 1e+05 has five characters, 100000 has six. Write {:.0f} or {:f} to get the digits written out.

  3. Question 3 of 6 "₹500" is 6 bytes in UTF-8. How many spaces does std::format("{:>8}", "₹500") put in front of it?

    Choose one answer.

    Show the answer to question 3

    Answer: Four, because the width counts columns and the rupee sign takes one

    For UTF-8 text the width is an estimate of the columns the text takes on screen, so "₹500" counts as 4 columns and gets 4 spaces. The result is 10 bytes long, which std::formatted_size reports.

  4. Question 4 of 6 r = std::format_to_n(buf, 12, "Total: {:.2f}", 1234567.891) writes into a 12-character buffer. The full result would be 17 characters. What is r.size?

    Choose one answer.

    Show the answer to question 4

    Answer: 17, the size of the whole result

    format_to_n writes at most n characters, and r.size is always the size the whole result would have. Comparing r.size with the buffer's size tells you whether anything was cut; r.out points just past the last character written.

  5. Question 5 of 6 A program reads the format string "{} of {}" from a settings file into a std::string fmt. Which call fills it in with C++23?

    Choose one answer.

    Show the answer to question 5

    Answer: std::vformat(fmt, std::make_format_args(done, total))

    std::format and std::print check their format string while compiling, so it must be a constant expression; a string from a file is not. std::vformat takes the string at run time and throws std::format_error if it is bad. In C++26, std::format(std::dynamic_format(fmt), done, total) does the same.

  6. Question 6 of 6 What does this line print?

    Read the code, then choose one answer.

    std::println("{:x} {:x}", -1, static_cast<unsigned>(-1));
    Show the answer to question 6

    Answer: -1 ffffffff

    std::format writes a negative number in any base with a minus sign followed by its magnitude, unlike printf's %x, which prints the bit pattern. Converting to an unsigned type first shows the 32 bits of -1, all ones.

Interview questions

Fresher · When would you use std::format_to or std::format_to_n instead of std::format?

std::format returns a new std::string, which needs memory of its own as soon as the result is longer than the string’s internal buffer: 15 characters with libstdc++ and 22 with libc++. std::format_to writes the same text through an output iterator, for example std::back_inserter into a string you clear and reuse, so a loop that formats a thousand rows into one reserved string allocates nothing. std::format_to_n writes at most n characters into a fixed buffer and returns where it stopped and how long the whole result is, so truncation can be detected; it suits stack buffers, C interfaces and code that may not use the heap. std::formatted_size gives the length first. All three take the same format strings as std::format, checked while compiling.

Key takeaways

  • A format specification is [[fill]align][sign][#][0][width][.precision][L][type], every part optional and always in that order; 0 pads only when there is no alignment.
  • {} writes a double in its shortest exact form and picks exponent notation when it is shorter, so 100000.0 prints as 1e+05; give a type such as {:.2f} when the digits matter.
  • Widths count columns, estimated from grapheme clusters, not bytes; {:?} escapes text for logs, and C++23 formats ranges and maps.
  • std::format_to and std::format_to_n write into memory you already have; format_to_n reports the full size so you can detect a cut.
  • Run-time format strings go through std::vformat and throw std::format_error; nested {} handles run-time widths without giving up the compile-time check.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.