Text and formatting
In this chapter we replace the old C‑style formatting functions with the modern, type‑safe facilities that arrive in C++20 and C++23. The progression mirrors the lessons from the lifetime chapters: first we look at why printf is hazardous, then we introduce owning strings, non‑owning views, and finally the compile‑time checked formatting API.
The C printf problem
printf takes a format string that describes the types of the arguments that follow. The compiler cannot verify that the format string matches the argument list because the string is an ordinary char const *. If the programmer writes a mismatched conversion specifier or omits an argument, the program exhibits undefined behavior at run time.
// The format string expects an `int` and a `double`, but only one argument is supplied.
printf("%d %f\n", 42)
The above code compiles, but at execution the function reads a non‑existent double from the stack. The result is nondeterministic and leads to memory corruption.
std::string and std::string_view
std::string owns the character storage. It allocates memory, frees it when the object is destroyed, and therefore always remains valid for the lifetime of the owning object.
std::string_view does not own any characters. It merely holds a pointer and a length. The view is valid only while the underlying characters remain alive. Because a view never copies, it is ideal for function parameters: the caller can pass a std::string, a string literal, or a substring without allocating a new buffer.
void greet(std::string_view name) {
std::println("Hello, {}!", name);
}
greet("Ada") // literal - no allocation
std::string full = "Grace Hopper"
greet(full) // temporary view of whole string
greet(full.substr(0,5)) // view of a prefix
The function greet never copies the characters it simply reads them through the view.
std::format (C++20)
std::format replaces printf with a type‑safe, variadic formatting function. The format string contains {} placeholders. The compiler parses the literal at compile time, matches each placeholder with the corresponding argument, and rejects mismatches with a diagnostic.
#include <format>
#include <print>
#include <string>
int main() {
std::string name = "Alice";
int age = 30;
double score = 95.5;
std::println("{}", std::format("Name: {}, Age: {}, Score: {:.2f}", name, age, score));
return 0;
}
Running the program prints a line that contains Name: Alice. The EXPECT test in the build system checks for that substring.
std::print and std::println (C++23)
std::print writes to stdout without appending a newline. std::println does the same but adds a newline automatically. The book uses std::println for line-oriented output. It uses std::print only when the output is a sequence of values on one line, written inside a loop, where a newline after each value is wrong. In that case the loop body calls std::print("{} ", value) and a single std::println() closes the line after the loop.
#include <print>
int main() {
std::println("{:>10} | {:>8}", "Item", "Value");
std::println("{:>10} | {:>8.2f}", "A", 1.23);
std::println("{:>10} | {:>8.2f}", "B", 4.56);
std::println("{:>10} | {:>8.2f}", "C", 7.89);
return 0;
}
The example prints a small table. The test harness looks for the token A in the output.
| Facility | Header | Type safety | Format checking | Returns | Use when |
|---|---|---|---|---|---|
std::cout | <iostream> | per insertion | none | stream reference | stream features, custom operator<< |
printf | <cstdio> | none | none | int count | C interop, legacy code |
std::print / std::println | <print> | typed arguments | compile time | void | new code, checked formatting |
Format specifiers
The part of the format string after a colon controls alignment, width, fill character, precision, and numeric base.
- Width and alignment:
{:<10}left‑aligns inside a field of ten characters,{:>10}right‑aligns,{:^10}centers. - Fill character:
{:*^8}pads with*while centering. - Precision:
{: .2f}prints a floating‑point value with two digits after the decimal point. - Base:
{:#x}prints an integer in hexadecimal with a0xprefix,{:08b}prints binary padded to eight digits.
std::println("{:>10}", "right") // " right"
std::println("{:08b}", 5) // "00000101"
std::println("{:#x}", 255) // "0xff"
std::println("{:.2f}", 3.14159) // "3.14"
If a specifier is unknown, the compiler issues an error because the format string is a compile‑time constant.
The specifier syntax mirrors Python’s format. The colon introduces a format‑spec, then optional fill, alignment, sign, alternate form, zero‑pad, width, precision, and type. The order matters. The compiler enforces it.
Performance of std::format is comparable to hand‑written printf for simple cases. The library avoids temporary allocations for short strings by using a small‑buffer optimisation.
Custom types can be formatted by specialising std::formatter. The specialization returns a format_to function that writes the representation into the provided output iterator.
struct Point { int x; int y; };
template<> struct std::formatter<Point> {
constexpr auto parse(auto& ctx) { return ctx.begin(); }
auto format(Point const& p, auto& ctx) const {
return std::format_to(ctx.out(), "({},{})", p.x, p.y);
}
};
std::println("{}", Point{3,4}); // prints "(3,4)"
The same custom formatter works for both std::println and std::format because they share the formatter protocol.
Advanced formatting features
Beyond the basic width and precision specifiers, std::format supports a rich set of options that let developers tailor the textual representation of values.
- Sign handling:
{: +}forces a leading plus sign for positive numbers, while{: -}(the default) prints a minus sign only for negatives. - Alternate form: The
#flag adds a prefix for certain types:0xfor hexadecimal,0for octal, and a trailing decimal point for floating‑point values. - Zero padding:
{:08}pads the field with zeros instead of spaces. It is equivalent to{:0>8}but more concise. - Grouping:
{:L}formats numbers according to the locale’s thousands separator. This works together with astd::localeoverload. - Date and time: When the
<chrono>library provides astd::chrono::year_month_dayorstd::chrono::hh_mm_ssobject, the formatter can emit ISO‑8601 strings using the{:T}or{:D}specifiers.
std::println("{:+08}", 42) // "+0000042"
std::println("{:#x}", 255) // "0xff"
std::println("{:L}", 1234567) // "1,234,567" in en_US locale
using namespace std::chrono
auto now = floor<seconds>(system_clock::now())
std::println("{:T}", now) // "2026-08-21T14:35:00"
These specifiers are composable a format string can combine alignment, fill, width, sign, and type in a single placeholder. The compiler checks that the combination is valid for the argument type, rejecting illegal mixes such as a sign flag on a string.
Custom types can also honour these flags by inspecting the format_context and the parsed format‑spec. A formatter can honour fill and align by delegating to std::format_to with a constructed format string, or it can implement the formatting logic directly for maximum performance.
struct Money {
int cents;
};
template<> struct std::formatter<Money> {
char presentation = 'f'; // f = dollars.cents, e = euros
constexpr auto parse(auto& ctx) {
auto it = ctx.begin();
if (it != ctx.end() && (*it == 'e' || *it == 'f')) presentation = *it++;
return it;
}
auto format(Myney const& m, auto& ctx) const {
if (presentation == 'e')
return std::format_to(ctx.out(), "€{:.2f}", m.cents / 100.0);
else
return std::format_to(ctx.out(), "${:.2f}", m.cents / 100.0);
}
};
std::println("{}", Money{1234}) // prints "$12.34"
The example demonstrates how a formatter can respect a presentation specifier while still supporting the generic alignment and width options supplied by the surrounding format string.
Placeholders can refer to arguments by position.
std::println("{0} + {0} = {1}", 2, 4) // prints "2 + 2 = 4"
Named arguments are not part of the core standard, but a user can achieve them by providing a custom std::formatter specialization or by using std::make_format_args together with a format string that references names via a library such as {fmt}. The book mentions the technique briefly because it is useful in larger projects.
// Using a custom formatter (illustrative - not compiled here)
struct Point { int x int y }
template<> struct std::formatter<Point> : std::formatter<std::string> {
auto format(Point const& p, auto& ctx) const {
return std::formatter<std::string>::format(
std::format("({},{})", p.x, p.y), ctx)
}
}
Compile‑time checking is the point
std::format only accepts a compile‑time constant format string for full checking. If a program needs a run‑time format, the library provides std::runtime_format, which disables compile‑time verification. This design forces the programmer to choose safety whenever possible.
Contrast the two approaches:
printf("%d %f\n", 42): compiles, can crash at run time.std::format("%d %f\n", 42): fails to compile because%is not a valid placeholder.std::format(std::runtime_format(fmt), 42): compiles, but the format string is unchecked.
The book’s theme is to let the compiler catch what it can, and std::format embodies that principle.
Performance and constexpr formatting
std::format is designed to be fast. The implementation parses the format string at compile time when the literal is a constant expression, eliminating runtime parsing overhead. This makes it comparable to hand‑written printf for simple cases while providing safety.
When the format string cannot be known at compile time, the library falls back to a runtime parser. The cost is modest: a single pass over the format string plus the usual formatting work. For tight loops where every nanosecond matters, developers can still write a custom printf‑style loop, but the safety trade‑off must be justified.
C++23 extends std::format with constexpr support. A constexpr function can call std::format to produce a compile‑time constant string that can be used as a non‑type template parameter or in a static_assert.
constexpr std::string_view make_label(int id) {
return std::format("Item-{:03}", id)
}
static_assert(make_label(7) == "Item-007")
The example illustrates how formatting can participate in compile‑time computation, enabling expressive metaprogramming without sacrificing safety.
Locale‑aware formatting is optional. By default std::format uses the “C” locale, which formats numbers with a period as the decimal separator. Passing a std::locale object to the overload selects the suitable digit grouping and decimal marks for the target locale.
std::locale german("de_DE")
std::println(std::format(german, "{:L}", 1234567.89)) // prints "1.234.567,89"
Custom formatters, shown earlier, work uniformly with locale‑aware overloads because the formatter receives the locale via its format method.
Error handling in formatting
When a format string is ill‑formed, std::format throws a std::format_error. This exception type is a subclass of std::runtime_error and carries a message that identifies the problem, for example, “argument index out of range” or “invalid format specifier”.
try {
std::println(std::format("{0} {2}", 1, 2)) // index 2 does not exist
} catch (const std::format_error& e) {
std::cerr << "Formatting failed: " << e.what() << '\n'
}
The library also provides the non‑throwing overload std::format_to_n which writes into a pre‑allocated buffer and returns the number of characters written. This is useful in low‑latency or embedded contexts where exceptions are disabled.
char buf[32]
auto result = std::format_to_n(buf, sizeof(buf), "{:04x}", 0x1A3)
std::println("{} characters written", result.out - buf)
result.out points just past the last character written, allowing the caller to construct a std::string_view without an extra copy.
The library also defines std::vformat and std::vprint which take a std::format_args object generated by std::make_format_args. These functions enable runtime‑determined argument lists while still performing compile‑time checks on the format string itself.
auto args = std::make_format_args(42, 3.14)
std::println(std::vformat("int={}, double={}", args))
If the format string itself is not a constant expression, the parser cannot validate the placeholders against the arguments at compile time. In that case, the same run‑time checks apply, and mismatches still result in a std::format_error.
The distinction between compile‑time guarantees and run‑time safety mirrors the earlier discussion of printf. By default the library favours compile‑time safety developers can opt into run‑time flexibility when the application requires it.
Try this
Write a program that prints a three‑row table. Each column must have a fixed width. The second column contains floating‑point numbers printed with two decimal places and right‑aligned.
// Insert your own code here - use std::println and format specifiers.
When you run the program, the output must look like a tidy table with aligned columns.
NOTE: All examples in this chapter are compiled and tested with the Clang 22 toolchain using
-std=c++26. Thebook_examplemacro registers each source file as a test target, and theEXPECTstrings verify that the output contains the expected fragments.