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 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}.

  • 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 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:

point_stream.cpp C++ · point_stream.cpp
// 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
}
Chained output, and a width that misses Python · stream_session.py
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:

point_format.cpp C++ · point_format.cpp
// 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
}
context_pitfall.cpp C++ · context_pitfall.cpp
// 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
}
The formatter at work, then the first-attempt version Python · format_session.py
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 whose begin() 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 returns ctx.begin() at once and accepts only {}. It is constexpr, because the compiler calls it while checking format strings.
  • format() writes the value through the output iterator ctx.out(), here with std::format_to, and returns the iterator where its output ends. It must be a const member: the standard lets the library call it on a const formatter, and both libraries refuse a formatter whose format() is not const.

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.

std::format checks "{:in}" with the formatter's parse() while compiling, then parses again and calls format() while running.std::format("{:in}", votes)While compilingWhile the program runs"12,34,56,789"The format string is checked:formatter<Count>::parse() reads "in}".A throw here is a compile error.A new formatter<Count> parses "in}"and remembers the Indian styleformat(votes, ctx) writes the digitsthrough ctx.out()it compiled

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.

  1. 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.
  2. 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.
  3. Then std::format calls format(votes, ctx), which writes the digits with their commas through the output iterator ctx.out().
  4. 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:

inherit_formatter.cpp C++ · inherit_formatter.cpp
// 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);
}
Options of double, applied to both coordinates Python · inherit_session.py
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:

count_format.cpp C++ · count_format.cpp
// 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});
}
One number, three groupings Python · count_session.py
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_locale.cpp C++ · indian_locale.cpp
// 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());
    }
}
A facet of your own, then the system locale Python · locale_session.py
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

Lakh Crore Converter (to Million, Billion) Compare any amount in Indian and international grouping, and in lakh, crore, million and billion.

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:

spec_errors.cpp C++ · spec_errors.cpp
// 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
runtime_spec.cpp C++ · runtime_spec.cpp
// 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());
        }
    }
}
Two compile errors, then the same checks at run time Python · errors_session.py
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:

  1. parse() accepts an empty specification, in and intl, and throws std::format_error for anything else.
  2. format() writes the rupees, a point and exactly two digits of paise: Rupees{123456789} prints as 1234567.89 with {}, 12,34,567.89 with {:in} and 1,234,567.89 with {:intl}.
  3. 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 a std::uint64_t.
The starter code and its sample tests

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:

The sample tests against the starter code Python · rupees_session.py
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.

Number Format Converter (1,234.56 ↔ 1.234,56 ↔ 12,34,567) See one number written with the digit grouping and decimal sign of many countries.

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 takes std::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 a constexpr parse(), which reads the specification and returns an iterator to its }, and a const format(), which writes through ctx.out().
  • Take the format context as auto&, so the formatter also works for ranges such as std::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_error in parse() makes an unknown one a compile error; strings known only at run time go through std::vformat and 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.

  1. Question 1 of 6 Why is std::ostream& operator<<(std::ostream&, const Point&) written as a non-member function?

    Choose one answer.

    Show the answer to question 1

    Answer: Its left operand is the stream, and a member would have to belong to std::ostream, which you cannot change

    For a member operator, the left operand is the object the function belongs to. In std::cout << p that is the stream, so the function would have to be a member of std::ostream. A non-member taking the stream first is the only way to add it.

  2. Question 2 of 6 operator<< for Point writes '(', then x, ", ", y and ')'. What does std::cout << std::setw(12) << Point{1.5, -2} pad?

    Choose one answer.

    Show the answer to question 2

    Answer: Only the opening bracket, to 12 characters

    A field width applies to the next formatted output only, and the stream resets it afterwards. Inside operator<< the next output is the single character '(', so the point appears after 11 spaces. Write the point into a std::ostringstream first to pad all of it.

  3. Question 3 of 6 For the placeholder {:intl}, what should a formatter's parse() return?

    Choose one answer.

    Show the answer to question 3

    Answer: An iterator pointing at the closing }

    parse() starts at the first character after the colon, reads the specification and returns an iterator to the first character it did not consume, which must be the closing } (or the end of the input).

  4. Question 4 of 6 This formatter prints a single Point with std::println("{}", p). What happens with std::println("{}", std::vector<Point>{{0, 0}})?

    Read the code, then choose one answer.

    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);
        }
    };
    Show the answer to question 4

    Answer: It does not compile, because format() accepts only std::format_context

    Range formatting checks that the element type can be formatted with a context whose output iterator is not std::format_context's. Declaring the parameter as auto& ctx makes format() a template that accepts every context, and then the vector prints as [(0, 0)].

  5. Question 5 of 6 parse() throws std::format_error for any specification but in and intl. What happens to the line auto s = std::format("{:lakh}", votes);?

    Choose one answer.

    Show the answer to question 5

    Answer: The program does not compile

    A literal format string is checked while compiling: its constructor runs parse() in a constant evaluation, and a throw there is not allowed, so the evaluation fails. The same string given to std::vformat would be checked only when the line runs, and would throw.

  6. Question 6 of 6 std::formatter<Point> derives from std::formatter<double> and formats both coordinates with the base class. What does std::println("{:.2f}", Point{1.5, -2.75}) print?

    Choose one answer.

    Show the answer to question 6

    Answer: (1.50, -2.75)

    The inherited parse() reads .2f exactly as for a double, and format() applies the stored options to each coordinate in turn, so both get two decimals.

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.