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.
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:
// 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));
}
} 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}prints42and four spaces: once an alignment is given, the0option does nothing.- The prefix counts towards the width:
{:#010b}gives0b00000101, ten characters in all. - A negative number keeps its minus sign in every base, so -1024 is
-0x400.{:x}of -1 prints-1, not theffffffffthatprintf("%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:
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.
- The type and the precision make the digits: the type f with the precision .2 rounds 3.14159 to "3.14".
- 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.
- 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.
- 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:
// 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));
} 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:
fandetake the precision as digits after the point (six by default), andgas significant digits, dropping zeros at the end.{:.2f}rounds the value that is stored, and 2.675 is stored as2.67499999999999982236…, so it prints2.67, asprintfdoes. 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.0keeps its sign, and infinity and NaN print asinfandnan.- A
floatgets the digits afloatneeds:0.1fprints as0.1, and the same value converted todoubleprints as0.10000000149011612.
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:
// 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);
} 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,\nand quote marks escaped. It is the right choice for log lines, where a hidden tab or line break would otherwise vanish.boolprints astrue, or as1with{:d}; acharprints as a character, or as its code with{:d}, and an integer prints as a character with{:c}. Pointers print in hexadecimal, andnullptras0x0.- 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.
Writing into buffers you already have
std::format returns a new string every time. Three functions let you decide where the characters go:
// 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);
} 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 mostncharacters and returns a pair:out, where the writing stopped, andsize, the length the whole result has. Here 12 characters went into the 12-character buffer, andsizewas 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. Withstd::back_inserterand one string that is cleared and reused, a thousand rows caused no allocation at all, whilestd::formatallocated 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 CFILE*, 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 withstd::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:
// 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);
} // 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);
} 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:
- 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. std::make_format_argskeeps references to its arguments, so they must be variables that live until the call returns. Passing a temporary, such as astd::stringbuilt in the call, does not compile with either library: paper P2905 made that a compile error because the reference would dangle.- 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:
amount(paise)writes rupees, a point and exactly two digits of paise:123456gives1234.56,1005gives10.05, and-50gives-0.50.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.closing_line(balance)writesClosing balanceleft-aligned in 34 columns and the balance right-aligned in 22.
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:
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.
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.
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;0pads only when there is no alignment. {}writes adoublein its shortest exact form and picks exponent notation when it is shorter, so100000.0prints as1e+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_toandstd::format_to_nwrite into memory you already have;format_to_nreports the full size so you can detect a cut.- Run-time format strings go through
std::vformatand throwstd::format_error; nested{}handles run-time widths without giving up the compile-time check.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- P0645R10: Text Formatting (ISO/IEC JTC1/SC22/WG21)
- P2093R14: Formatted output (ISO/IEC JTC1/SC22/WG21)
- P2216R3: std::format improvements (ISO/IEC JTC1/SC22/WG21)
- P2286R8: Formatting Ranges (ISO/IEC JTC1/SC22/WG21)
- P2905R2: Runtime format strings (ISO/IEC JTC1/SC22/WG21)
- P2918R2: Runtime format strings II (ISO/IEC JTC1/SC22/WG21)
- P3953R3: Rename std::runtime_format (ISO/IEC JTC1/SC22/WG21)
- P3505R4: Fix the default floating-point representation in std::format (ISO/IEC JTC1/SC22/WG21)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress