C++ Module 7 – Classes, objects and RAII
Printable types: operator<< and std::formatter
Print your own C++ types with operator<< for streams and a std::formatter for std::format and std::print, with format options of your own such as {:in}.
What you will learn
- Write operator<< so that a type prints to any output stream
- Implement a std::formatter specialisation so that std::format and std::print accept a type
- Reuse the options of a standard formatter by deriving from it
- Parse a format specification of your own and reject unknown ones while compiling
Before you start
What you’ll learn · Before you start
What you will learn
- Write operator<< so that a type prints to any output stream
- Implement a std::formatter specialisation so that std::format and std::print accept a type
- Reuse the options of a standard formatter by deriving from it
- Parse a format specification of your own and reject unknown ones while compiling
Before you start
On this page
C++ has two ways to print a value, and a type of your own needs one piece of code for each. For iostreams, such
as std::cout << p, you write a non-member function operator<< that takes the stream and the value, writes the
value’s parts and returns the stream. For std::format, std::print and std::println, you specialise the
class template std::formatter for your type. A formatter has two member functions: parse() reads the format
specification, the text between : and } in a placeholder such as {:.2f}, and format() writes the value. Format
strings are checked while the program compiles, so a mistake in a placeholder is a compile error rather than wrong
output.
This lesson prints a 2D point both ways, reuses the options of double for it, gives a counting type its own
{:in} option for Indian digit grouping, and shows which mistakes each compiler catches. Every example runs with
Apple clang 21 and GCC 16.
operator<< for streams
operator<< for a stream takes the stream by reference as its first parameter and the value, usually as a
reference to const, as its second. The left operand is the stream, which you cannot add members to, so the
function is always a non-member:
// operator<< for a Point, and what a field width does to it.
#include <iomanip>
#include <iostream>
#include <sstream>
struct Point {
double x = 0;
double y = 0;
};
// The stream is the left operand, so this cannot be a member of Point.
std::ostream& operator<<(std::ostream& out, const Point& p) {
return out << '(' << p.x << ", " << p.y << ')';
}
int main() {
Point a{1.5, -2};
Point b{3, 4};
std::cout << a << " and " << b << '\n'; // each << returns the stream, so the calls chain
std::cout << '[' << std::setw(12) << a << "]\n"; // the width pads only the first thing written: '('
std::ostringstream text; // write the whole point first ...
text << a;
std::cout << '[' << std::setw(12) << text.str() << "]\n"; // ... then pad the finished text
} from session import run
# point_stream.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra point_stream.cpp -o point_stream && ./point_stream
g++-16 -std=c++23 -Wall -Wextra point_stream.cpp -o point_stream-gcc && ./point_stream-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra point_stream.cpp -o point_stream && ./point_stream (1.5, -2) and (3, 4) [ (1.5, -2)] [ (1.5, -2)] $ g++-16 -std=c++23 -Wall -Wextra point_stream.cpp -o point_stream-gcc && ./point_stream-gcc (1.5, -2) and (3, 4) [ (1.5, -2)] [ (1.5, -2)]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stream_session.py
Returning the stream is what lets std::cout << a << " and " << b chain: each << hands the same stream to the
next one. The function should write the value and nothing else. A trailing newline would surprise every caller who
prints two points on one line.
The second line of output shows a trap. std::setw(12) sets the width of the next thing written to the stream,
and the stream resets the width afterwards. Inside operator<< the next thing is the single character (, which is
padded to 12 characters, and the rest of the point follows unpadded. To pad the whole point, write it into a
std::ostringstream first and pad the finished text, as the third line does. A formatter has no such hidden state:
it receives the whole specification and decides itself what a width means for its output.
A formatter for std::format and std::print
std::format finds a type’s printing code in a specialisation of std::formatter, declared in <format>. You
specialise it for your type, a rare case in which user code adds to namespace std, and the standard allows exactly
that:
// A std::formatter for Point: std::format, std::print and std::println accept it.
#include <format>
#include <print>
#include <string>
#include <vector>
struct Point {
double x = 0;
double y = 0;
};
template <>
struct std::formatter<Point> {
// Reads the format specification, the text between ':' and '}'. This one accepts none: only {}.
constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
// Writes the point and returns where the output ends. Any context type, so that ranges can use it too.
auto format(const Point& p, auto& ctx) const { return std::format_to(ctx.out(), "({}, {})", p.x, p.y); }
};
int main() {
Point a{1.5, -2};
std::println("a = {}", a);
std::string label = std::format("start {} end {}", a, Point{3, 4});
std::println("{} ({} characters)", label, label.size());
std::vector<Point> path{{0, 0}, {1.5, -2}, {3, 4}};
std::println("path {}", path); // C++23 formats ranges, element by element
} // The same formatter with format() taking std::format_context& works for one Point, not for a vector of them.
#include <format>
#include <print>
#include <vector>
struct Point {
double x = 0;
double y = 0;
};
template <>
struct std::formatter<Point> {
constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
auto format(const Point& p, std::format_context& ctx) const { return std::format_to(ctx.out(), "({}, {})", p.x, p.y); }
};
int main() {
std::println("{}", Point{1.5, -2}); // fine
std::println("{}", std::vector<Point>{{0, 0}}); // error: Point is not formattable inside a range
} from session import run
# point_format.cpp with both compilers, then the errors that point at context_pitfall.cpp and the library's
# own message (sed shortens the header's folder to its file name).
run("""
clang++ -std=c++23 -Wall -Wextra point_format.cpp -o point_format && ./point_format
g++-16 -std=c++23 -Wall -Wextra point_format.cpp -o point_format-gcc && ./point_format-gcc
clang++ -std=c++23 -fsyntax-only context_pitfall.cpp 2>&1 | grep -E "^context_pitfall.cpp:[0-9:]+ error|static assertion failed" | sed -E 's#^/[^:]*/##' | head -3
g++-16 -std=c++23 -fsyntax-only context_pitfall.cpp 2>&1 | grep -E "^context_pitfall.cpp:[0-9:]+ error|static assertion failed" | sed -E 's#^/[^:]*/##' | head -3
""") Output
$ clang++ -std=c++23 -Wall -Wextra point_format.cpp -o point_format && ./point_format
a = (1.5, -2)
start (1.5, -2) end (3, 4) (26 characters)
path [(0, 0), (1.5, -2), (3, 4)]
$ g++-16 -std=c++23 -Wall -Wextra point_format.cpp -o point_format-gcc && ./point_format-gcc
a = (1.5, -2)
start (1.5, -2) end (3, 4) (26 characters)
path [(0, 0), (1.5, -2), (3, 4)]
$ clang++ -std=c++23 -fsyntax-only context_pitfall.cpp 2>&1 | grep -E "^context_pitfall.cpp:[0-9:]+ error|static assertion failed" | sed -E 's#^/[^:]*/##' | head -3
context_pitfall.cpp:19:18: error: call to consteval function 'std::basic_format_string<char, std::vector<Point>>::basic_format_string<char[3]>' is not a constant expression
format_arg_store.h:175:17: error: static assertion failed due to requirement '__arg != __arg_t::__none': the supplied type is not formattable
format_arg_store.h:176:17: error: static assertion failed
$ g++-16 -std=c++23 -fsyntax-only context_pitfall.cpp 2>&1 | grep -E "^context_pitfall.cpp:[0-9:]+ error|static assertion failed" | sed -E 's#^/[^:]*/##' | head -3
format:5208:10: error: static assertion failed: std::formatter must be specialized for each type being formatted
context_pitfall.cpp:19:18: error: call to consteval function 'std::basic_format_string<char, std::vector<Point, std::allocator<Point> > >("{}")' is not a constant expression
format:4829:25: error: static assertion failed: std::formatter must be specialized for the type of each format arg
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 format_session.py
The two member functions divide the work:
parse()receives a parse context whosebegin()points at the format specification, the characters after the:, or at the}of a plain{}. It reads what it understands and returns an iterator to the closing}. This formatter understands nothing, so it returnsctx.begin()at once and accepts only{}. It isconstexpr, because the compiler calls it while checking format strings.format()writes the value through the output iteratorctx.out(), here withstd::format_to, and returns the iterator where its output ends. It must be aconstmember: the standard lets the library call it on aconstformatter, and both libraries refuse a formatter whoseformat()is notconst.
Once the specialisation exists, std::println, std::format and std::format_to all accept Point, and so does
C++23’s range formatting: path prints as a list, [(0, 0), (1.5, -2), (3, 4)], with the point formatter used for
each element.
context_pitfall.cpp is the version many people write first, with format() taking std::format_context&. A single
Point prints, but the vector does not compile. Formatting a range checks whether its element type can be formatted
with a context of another, unspecified output type, and a function fixed to std::format_context cannot accept that.
Taking the context as auto&, as point_format.cpp does, makes format() a template that works with every context.
When the two halves of a formatter run
Text description of the diagram
The diagram follows std::format("{:in}", votes), where votes is a Count with a formatter of its own.
- While compiling, the format string is checked. The compiler calls formatter<Count>::parse() on the specification "in}" in a constant evaluation. If parse() throws std::format_error, the evaluation fails and the program does not compile.
- While the program runs, std::format creates a new formatter<Count>, calls parse() on "in}" again, and the formatter remembers that it should use the Indian style.
- Then std::format calls format(votes, ctx), which writes the digits with their commas through the output iterator ctx.out().
- The result is the string "12,34,56,789".
Reusing an existing formatter
Writing your own parse() means deciding which options to support. Often the options of a standard type are exactly
right. A formatter derived from std::formatter<double> inherits its parse(), so every option a double accepts,
such as precision, width, alignment and sign, is accepted for Point too, and checked in the same way:
// Deriving from std::formatter<double> gives a Point every option a double has.
#include <format>
#include <print>
struct Point {
double x = 0;
double y = 0;
};
template <>
struct std::formatter<Point> : std::formatter<double> {
// parse() is inherited: it reads "{:.2f}" or "{:>8.1f}" as it would for a double.
auto format(const Point& p, auto& ctx) const {
ctx.advance_to(std::format_to(ctx.out(), "("));
ctx.advance_to(std::formatter<double>::format(p.x, ctx)); // the options apply to x ...
ctx.advance_to(std::format_to(ctx.out(), ", "));
ctx.advance_to(std::formatter<double>::format(p.y, ctx)); // ... and to y
return std::format_to(ctx.out(), ")");
}
};
int main() {
Point p{1.5, -2.75};
std::println("{}", p);
std::println("{:.2f}", p);
std::println("{:>8.1f}", p);
std::println("{:+.0e}", p);
} from session import run
# inherit_formatter.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra inherit_formatter.cpp -o inherit_formatter && ./inherit_formatter
g++-16 -std=c++23 -Wall -Wextra inherit_formatter.cpp -o inherit_formatter-gcc && ./inherit_formatter-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra inherit_formatter.cpp -o inherit_formatter && ./inherit_formatter (1.5, -2.75) (1.50, -2.75) ( 1.5, -2.8) (+2e+00, -3e+00) $ g++-16 -std=c++23 -Wall -Wextra inherit_formatter.cpp -o inherit_formatter-gcc && ./inherit_formatter-gcc (1.5, -2.75) (1.50, -2.75) ( 1.5, -2.8) (+2e+00, -3e+00)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 inherit_session.py
format() writes the brackets and the comma itself and hands each coordinate to the base class’s format(), so
{:.2f} gives both coordinates two decimals and {:>8.1f} pads each of them to eight characters. After every piece
of output, ctx.advance_to() tells the context where the output now ends, so that the next piece goes after it.
Here a width applies to each coordinate rather than to the whole point, because that is what this format() does
with it; a formatter decides how its options act on its parts.
Your own format specification
A formatter can also define a small language of its own. Count holds a number of things, such as votes. Its
formatter accepts three specifications: {} prints the bare digits, {:intl} groups them in threes from the right,
as most of the world writes large numbers, and {:in} groups them the Indian way:
// A formatter with its own specification: {} plain, {:in} Indian grouping, {:intl} international grouping.
#include <cstdint>
#include <format>
#include <print>
#include <string>
#include <string_view>
struct Count {
std::uint64_t n = 0;
};
// Commas in a string of digits: threes from the right (international), or three and then twos (Indian).
std::string group_digits(const std::string& digits, bool indian) {
std::string out;
for (std::size_t i = 0; i < digits.size(); ++i) {
out.push_back(digits[i]);
std::size_t left = digits.size() - 1 - i; // digits still to come
bool comma = indian ? (left == 3 || (left > 3 && (left - 3) % 2 == 0)) : (left > 0 && left % 3 == 0);
if (comma) out.push_back(',');
}
return out;
}
template <>
struct std::formatter<Count> {
enum class Style { plain, indian, international };
Style style = Style::plain;
// ctx.begin() points just after the ':' (or at the '}' of a plain {}). Return where the specification ends.
constexpr auto parse(std::format_parse_context& ctx) {
auto end = ctx.begin();
while (end != ctx.end() && *end != '}') ++end;
std::string_view spec(ctx.begin(), end);
if (spec == "in") style = Style::indian;
else if (spec == "intl") style = Style::international;
else if (!spec.empty()) throw std::format_error("Count takes {}, {:in} or {:intl}");
return end;
}
auto format(const Count& c, auto& ctx) const {
std::string digits = std::to_string(c.n);
if (style != Style::plain) digits = group_digits(digits, style == Style::indian);
return std::format_to(ctx.out(), "{}", digits);
}
};
int main() {
Count votes{123456789};
std::println("{}", votes);
std::println("{:in}", votes);
std::println("{:intl}", votes);
std::println("{:in} and {:in}", Count{100000}, Count{999});
} from session import run
# count_format.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra count_format.cpp -o count_format && ./count_format
g++-16 -std=c++23 -Wall -Wextra count_format.cpp -o count_format-gcc && ./count_format-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra count_format.cpp -o count_format && ./count_format 123456789 12,34,56,789 123,456,789 1,00,000 and 999 $ g++-16 -std=c++23 -Wall -Wextra count_format.cpp -o count_format-gcc && ./count_format-gcc 123456789 12,34,56,789 123,456,789 1,00,000 and 999
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 count_session.py
parse() stores its decision in a member, style, and format() reads it later. That works because the library
runs parse() on a formatter object and then calls format() on that same object. Anything that parse() does not
recognise ends with throw std::format_error(...), which the next section turns into a compile error.
Lakh and crore
Indian writing groups the last three digits and then pairs: one lakh is 1,00,000 (a hundred thousand) and one crore
is 1,00,00,000 (ten million), so 123456789 is written 12,34,56,789. The standard format specification has no option
for that. Its L option groups digits the way a locale says, and a std::numpunct facet whose grouping string is
"\3\2" (three digits, then twos) produces Indian grouping with both libraries. Named system locales are less
dependable: on the Mac used for this lesson, Apple’s library finds an en_IN.UTF-8 locale, while GCC 16’s library
on the same machine rejects that name with std::runtime_error, as the run shows. A formatter that does the grouping
itself prints the same everywhere.
// Indian grouping through a locale: a numpunct facet of your own, then the system's en_IN locale if there is one.
#include <format>
#include <locale>
#include <print>
#include <stdexcept>
#include <string>
// Groups of three digits first, then of two: the grouping string "\3\2" says so.
struct IndianGrouping : std::numpunct<char> {
char do_thousands_sep() const override { return ','; }
std::string do_grouping() const override { return "\3\2"; }
};
int main() {
std::locale indian(std::locale::classic(), new IndianGrouping); // the locale owns and deletes the facet
std::println("own facet: {}", std::format(indian, "{:L}", 123456789));
std::println("own facet: {}", std::format(indian, "{:L}", 100000));
std::println("no L: {}", std::format(indian, "{}", 123456789));
try {
std::locale system("en_IN.UTF-8");
std::println("en_IN.UTF-8: {}", std::format(system, "{:L}", 123456789));
} catch (const std::runtime_error& e) {
std::println("en_IN.UTF-8: std::runtime_error: {}", e.what());
}
} from session import run
# indian_locale.cpp built and run with both compilers, on the same Mac.
run("""
clang++ -std=c++23 -Wall -Wextra indian_locale.cpp -o indian_locale && ./indian_locale
g++-16 -std=c++23 -Wall -Wextra indian_locale.cpp -o indian_locale-gcc && ./indian_locale-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra indian_locale.cpp -o indian_locale && ./indian_locale own facet: 12,34,56,789 own facet: 1,00,000 no L: 123456789 en_IN.UTF-8: 12,34,56,789 $ g++-16 -std=c++23 -Wall -Wextra indian_locale.cpp -o indian_locale-gcc && ./indian_locale-gcc own facet: 12,34,56,789 own facet: 1,00,000 no L: 123456789 en_IN.UTF-8: std::runtime_error: locale::facet::_S_create_c_locale name not valid
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 locale_session.py
Mistakes caught while compiling
std::format checks its format string while the program compiles (paper P2216R3). The string’s constructor calls
your parse() in a constant evaluation, and a throw cannot happen there, so an unknown specification becomes a
compile error. The same check counts the arguments:
// Two mistakes in format strings, both found while compiling.
#include <cstdint>
#include <format>
#include <string>
struct Count {
std::uint64_t n = 0;
};
template <>
struct std::formatter<Count> {
constexpr auto parse(std::format_parse_context& ctx) {
auto end = ctx.begin();
while (end != ctx.end() && *end != '}') ++end;
std::string_view spec(ctx.begin(), end);
if (spec != "" && spec != "in" && spec != "intl") throw std::format_error("Count takes {}, {:in} or {:intl}");
return end;
}
auto format(const Count& c, auto& ctx) const { return std::format_to(ctx.out(), "{}", c.n); }
};
std::string a = std::format("{:lakh}", Count{5}); // a specification Count does not know
std::string b = std::format("{} of {}", 3); // two replacement fields, one argument // A format string known only at run time: std::vformat checks it then, and throws std::format_error.
#include <cstdint>
#include <format>
#include <print>
#include <string>
struct Count {
std::uint64_t n = 0;
};
template <>
struct std::formatter<Count> {
bool grouped = false;
constexpr auto parse(std::format_parse_context& ctx) {
auto end = ctx.begin();
while (end != ctx.end() && *end != '}') ++end;
std::string_view spec(ctx.begin(), end);
if (spec == "intl") grouped = true;
else if (!spec.empty()) throw std::format_error("Count takes {} or {:intl}");
return end;
}
auto format(const Count& c, auto& ctx) const {
std::string s = std::to_string(c.n);
if (grouped)
for (int i = static_cast<int>(s.size()) - 3; i > 0; i -= 3) s.insert(static_cast<std::size_t>(i), ",");
return std::format_to(ctx.out(), "{}", s);
}
};
int main() {
Count c{2500000};
for (std::string fmt : {"{}", "{:intl}", "{:lakh}", "{} {}"}) {
try {
std::println("{:<8} -> {}", fmt, std::vformat(fmt, std::make_format_args(c)));
} catch (const std::format_error& e) {
std::println("{:<8} -> std::format_error: {}", fmt, e.what());
}
}
} from session import run
# The diagnostics that point into spec_errors.cpp, from each compiler, then runtime_spec.cpp.
run("""
clang++ -std=c++23 -fsyntax-only spec_errors.cpp 2>&1 | grep -E "^spec_errors.cpp:[0-9:]+ (error|note)"
g++-16 -std=c++23 -fsyntax-only spec_errors.cpp 2>&1 | grep -E "^spec_errors.cpp:[0-9:]+ (error|note)"
clang++ -std=c++23 -Wall -Wextra runtime_spec.cpp -o runtime_spec && ./runtime_spec
g++-16 -std=c++23 -Wall -Wextra runtime_spec.cpp -o runtime_spec-gcc && ./runtime_spec-gcc
""") Output
$ clang++ -std=c++23 -fsyntax-only spec_errors.cpp 2>&1 | grep -E "^spec_errors.cpp:[0-9:]+ (error|note)"
spec_errors.cpp:22:29: error: call to consteval function 'std::basic_format_string<char, Count>::basic_format_string<char[8]>' is not a constant expression
spec_errors.cpp:16:59: note: subexpression not valid in a constant expression
spec_errors.cpp:22:29: note: in call to 'basic_format_string<char[8]>("{:lakh}")'
spec_errors.cpp:23:29: error: call to consteval function 'std::basic_format_string<char, int>::basic_format_string<char[9]>' is not a constant expression
spec_errors.cpp:23:29: note: in call to 'basic_format_string<char[9]>("{} of {}")'
$ g++-16 -std=c++23 -fsyntax-only spec_errors.cpp 2>&1 | grep -E "^spec_errors.cpp:[0-9:]+ (error|note)"
spec_errors.cpp:22:29: error: call to consteval function 'std::basic_format_string<char, Count>("{:lakh}")' is not a constant expression
spec_errors.cpp:16:59: error: expression '<throw-expression>' is not a constant expression
spec_errors.cpp:23:29: error: call to consteval function 'std::basic_format_string<char, int>("{} of {}")' is not a constant expression
$ clang++ -std=c++23 -Wall -Wextra runtime_spec.cpp -o runtime_spec && ./runtime_spec
{} -> 2500000
{:intl} -> 2,500,000
{:lakh} -> std::format_error: Count takes {} or {:intl}
{} {} -> std::format_error: The argument index value is too large for the number of arguments supplied
$ g++-16 -std=c++23 -Wall -Wextra runtime_spec.cpp -o runtime_spec-gcc && ./runtime_spec-gcc
{} -> 2500000
{:intl} -> 2,500,000
{:lakh} -> std::format_error: Count takes {} or {:intl}
{} {} -> std::format_error: format error: invalid arg-id in format string
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py
Both compilers point at the line with "{:lakh}" and at the throw in parse() that made it fail, and at the
format string with two placeholders and one argument. This check only works because the format string is a literal
that the compiler can see.
A format string chosen while the program runs, say a template that users may edit, goes to std::vformat instead,
with its arguments packed by std::make_format_args. It is checked when it is used, and a bad one throws
std::format_error, which runtime_spec.cpp catches. The two libraries word their messages differently, so test for
the exception type, never for its text. Treat such a string as untrusted input: even a valid one can ask for a width
of a million characters.
Your turn: a formatter for rupees
Rupees holds an amount in paise, 100 to the rupee, like the Money type of the operator lesson. Complete its
formatter in rupees.h, which already contains the group_digits() function from count_format.cpp:
parse()accepts an empty specification,inandintl, and throwsstd::format_errorfor anything else.format()writes the rupees, a point and exactly two digits of paise:Rupees{123456789}prints as1234567.89with{},12,34,567.89with{:in}and1,234,567.89with{:intl}.- A negative amount starts with a minus sign, including the most negative one, -9,223,372,036,854,775,808 paise.
Its negation does not fit in a
std::int64_t, so compute the absolute value as astd::uint64_t.
C header · rupees.h
// Starter code for "Your turn": complete parse() and format() of std::formatter<Rupees>.
#pragma once
#include <cstdint>
#include <format>
#include <string>
#include <string_view>
// An amount of money in paise: Rupees{123456789} is 1234567 rupees and 89 paise.
struct Rupees {
std::int64_t paise = 0;
};
// Commas in a string of digits: threes from the right (international), or three and then twos (Indian).
inline std::string group_digits(const std::string& digits, bool indian) {
std::string out;
for (std::size_t i = 0; i < digits.size(); ++i) {
out.push_back(digits[i]);
std::size_t left = digits.size() - 1 - i; // digits still to come
bool comma = indian ? (left == 3 || (left > 3 && (left - 3) % 2 == 0)) : (left > 0 && left % 3 == 0);
if (comma) out.push_back(',');
}
return out;
}
template <>
struct std::formatter<Rupees> {
enum class Style { plain, indian, international };
Style style = Style::plain;
constexpr auto parse(std::format_parse_context& ctx) {
// TODO: accept the specifications "in" and "intl", and throw std::format_error for any other.
return ctx.begin();
}
auto format(const Rupees& r, auto& ctx) const {
// TODO: write the rupees, a point and two digits of paise ("1234567.89"), a minus sign before a negative
// amount, and the rupees grouped with group_digits() for {:in} and {:intl}.
return std::format_to(ctx.out(), "{}", r.paise);
}
}; C++ · rupees_tests.cpp
// Sample tests for rupees.h. Build: clang++ -std=c++23 -Wall -Wextra rupees_tests.cpp -o rupees_tests
#include <cstdint>
#include <format>
#include <limits>
#include <print>
#include <string>
#include <string_view>
#include <vector>
#include "rupees.h"
int checks = 0;
int failures = 0;
void check(bool ok, const char* what) {
++checks;
if (!ok) {
++failures;
std::println("FAIL {}", what);
}
}
// Formats with a format string known only at run time, so that this file builds before parse() is finished.
// A std::format_error comes back as text that the checks can compare.
template <class T>
std::string run_format(std::string_view fmt, const T& value) {
try {
return std::vformat(fmt, std::make_format_args(value));
} catch (const std::format_error& e) {
return std::string("format_error: ") + e.what();
}
}
int main() {
check(run_format("{}", Rupees{123456789}) == "1234567.89", "{} prints rupees and paise without commas");
check(run_format("{:in}", Rupees{123456789}) == "12,34,567.89", "{:in} groups three digits, then twos");
check(run_format("{:intl}", Rupees{123456789}) == "1,234,567.89", "{:intl} groups in threes");
check(run_format("{:in}", Rupees{5}) == "0.05", "five paise keep two digits after the point");
check(run_format("{:intl}", Rupees{-50}) == "-0.50", "a negative amount under one rupee");
check(run_format("{:in}", Rupees{100000000}) == "10,00,000.00", "ten lakh rupees");
check(run_format("{:in}", Rupees{std::numeric_limits<std::int64_t>::min()}) == "-92,23,37,20,36,85,47,758.08", "the most negative amount");
check(run_format("{:lakh}", Rupees{1}).starts_with("format_error"), "an unknown specification is rejected");
check(run_format("{::in}", std::vector<Rupees>{{100000}, {250}}) == "[1,000.00, 2.50]", "a vector, each element with {:in}");
std::println("{} of {} checks passed", checks - failures, checks);
return failures == 0 ? 0 : 1;
} The tests format through std::vformat, so that they build before parse() accepts in, and the last one prints a
vector with {::in}: the specification after the second colon applies to each element. Before you change anything,
only the check that an unknown specification is rejected passes:
from session import run
# The sample tests run against the starter code, before any TODO is done.
run("""
clang++ -std=c++23 -Wall -Wextra rupees_tests.cpp -o rupees_tests && ./rupees_tests
g++-16 -std=c++23 -Wall -Wextra rupees_tests.cpp -o rupees_tests-gcc && ./rupees_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra rupees_tests.cpp -o rupees_tests && ./rupees_tests
FAIL {} prints rupees and paise without commas
FAIL {:in} groups three digits, then twos
FAIL {:intl} groups in threes
FAIL five paise keep two digits after the point
FAIL a negative amount under one rupee
FAIL ten lakh rupees
FAIL the most negative amount
FAIL a vector, each element with {:in}
1 of 9 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra rupees_tests.cpp -o rupees_tests-gcc && ./rupees_tests-gcc
FAIL {} prints rupees and paise without commas
FAIL {:in} groups three digits, then twos
FAIL {:intl} groups in threes
FAIL five paise keep two digits after the point
FAIL a negative amount under one rupee
FAIL ten lakh rupees
FAIL the most negative amount
FAIL a vector, each element with {:in}
1 of 9 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 rupees_session.py
When both compilers report 9 of 9 checks passed, rebuild the tests with -fsanitize=undefined added: if your code
negates the amount as a signed number, the sanitizer reports the overflow on the most negative amount, even when the
output looks right. Finally, try std::println("{:in}", Rupees{5000}) in a
program of your own; once parse() knows in, it compiles, and {:lakh} does not.
Interview questions
Fresher · What is the difference between writing operator<< for a type and specialising std::formatter for it?
They serve different output systems. operator<< is a non-member function that writes the value to a
std::ostream, so std::cout << p and any other stream work; it uses the stream’s state, such as the width and
precision manipulators. A std::formatter<T> specialisation serves std::format, std::print and std::println:
its parse() reads the format specification in a placeholder such as {:.2f}, and its format() writes the value.
Format strings are checked while compiling, so an unknown specification is a compile error, and C++23 also formats
ranges of the type, such as a vector of them, once the formatter exists. A type that should print both ways needs
both pieces of code; one can call the other, for example a formatter that writes the result of an existing
operator<<.
Key takeaways
operator<<for streams is a non-member that takesstd::ostream&and the value, writes the value only, and returns the stream so calls chain. A field width reaches only its first piece of output.- A
std::formatter<T>specialisation has aconstexpr parse(), which reads the specification and returns an iterator to its}, and aconst format(), which writes throughctx.out(). - Take the format context as
auto&, so the formatter also works for ranges such asstd::vector<T>. - Deriving from
std::formatter<double>or another standard formatter inherits all of its options. - A specification of your own is parsed by hand, and a
throw std::format_errorinparse()makes an unknown one a compile error; strings known only at run time go throughstd::vformatand throw instead.
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.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- P0645R10: Text Formatting (ISO/IEC JTC1/SC22/WG21)
- P2216R3: std::format improvements (ISO/IEC JTC1/SC22/WG21)
- P2286R8: Formatting Ranges (ISO/IEC JTC1/SC22/WG21)
- P2093R14: Formatted output (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: C.167, use an operator for an operation with its conventional meaning (Standard C++ Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress