Sign up for the KDAB Newsletter
Stay on top of the latest news, publications, events and more.
Go to Sign-up
10 July 2025
We all need to log out data from time to time whilst developing or even when running in production. Sometimes this will be to stdout
, stderr
, a logfile, the system logger etc. Using tools such as spdlog
and fmt
make this a breeze. However, one thing has been causing an itch that I've wanted to scratch for a while now. How can we do nicely formatted printing of nested structs and maintaining indentation?
Given some simple nested structs such as:
struct Vec2 {
int x;
int y;
};
struct Size {
int width;
int height;
};
struct Rect {
Vec2 position;
Size size;
};
...
Rect rect{ { 0, 20 }, { 800, 600 } };
spdlog::info("rect: {}", rect);
We would like to easily obtain output similar to this:
rect: Rect2 {
position: Vec2 {
x: 0
y: 20
}
size: Size2 {
width: 800
height: 600
}
}
We hope this short blog post can show you a way to do exactly that using fmt
in a way that you can easily extend to your own types and customise further. This can then be used with spdlog
or similar to make your log files and debug output kinder to humans.
The excellent fmt documentation explains how to provide support for formatting your own types by providing a specialization of the fmt::formatter
template and taking advantage of subclassing or composing an existing fmt::formatter
to take advantage of the facilities they provide (e.g. parsing formatting specifications).
The slight design issue we have here is that fmt
is oriented around the concept of formatting independent lines of output. We need to have some state or context that is persistent across multiple lines, namely the amount to indent.
Examining the above layout of the output we see that we can break the output of a struct down to the following steps:
Our approach to this, is to provide a new class that specializes the fmt::formatter
template that manages the above steps. In order to easily allow customisation to output your own types taking advantage of the indenting facilities, we allow inheriting from our custom formatter using the Curiously Recurring Template Pattern (CRTP). This nicely allows customisation without any runtime overhead from virtual functions and allows us to provide most of the boilerplate in the base class.
If you just want to see the final code, you can play with it here.
To specialize the fmt::formatter
template we must provide two functions:
constexpr auto parse(fmt::format_parse_context &ctx)
-> fmt::format_parse_context::iterator
auto format(auto v, fmt::format_context &ctx) const
-> fmt::format_context::iterator
We won't go into the gory details of the former in this article. You can read it for yourself in the final code if you are interested. Suffice it to say it is concerned with extracting the initial indentation level if the user specifies one.
The format function in the indenting formatter is responsible for orchestrating the algorithm outlined above. Let's tackle the easy part first: outputting the type name and the opening curly brace. We require the developer to subclass using the CRTP which we can then make use of to output the custom type's name via a name()
member function on the derived class:
auto format(auto v, fmt::format_context &ctx) const
-> fmt::format_context::iterator
{
const T &derived = static_cast<const T &>(\*this);
fmt::format_to(ctx.out(), "{} {{\\n", derived.name());
// Get indentation level and output members
...
// Output the closing curly brace and return the context iterator
...
}
The fmt::format_to
function works just like the regular fmt::format
function except that it outputs to an output iterator which in this case is part of the format_context we are provided with. The remaining arguments are the format specification followed by any variables to be substituted in. For more information, there is an excellent guide to the fmt syntax.
To output the type name we obtain a reference to the derived object and call the derived.name()
member function. The format specification just says to output the substituted type name, followed by a literal curly brace, followed by a newline.
The parse functions provided by formatters are responsible for extracting the formatting instructions and keeping track of explicitly numbered arguments. Using explicitly numbered arguments allows the substitutions to appear in an explicit order rather than the implicit left-to-right ordering in the format specification. Within our format function, we must handle the case of numbered arguments when deciding upon which indentation level to use. To do so, we can lookup argument values from the format specification and operate upon them.
auto format(auto v, fmt::format_context &ctx) const
-> fmt::format_context::iterator
{
// Output type name
...
// Extract indentation level to use
size_t indentToUse;
if (!hasArgumentIndex) {
// Implicit argument ordering - use indent from parse() function
indentToUse = indent;
} else {
// Numbered argument ordering
auto argValue = ctx.arg(argumentIndex);
auto extractSizeT = \[\]<typename U>(const U &v) -> size_t {
if constexpr (std::is_integral_v<U>) {
return v;
} else {
throw std::format_error("Wrong argument datatype provided");
return {};
}
};
indentToUse = fmt::visit_format_arg(extractSizeT, argValue);
}
// Output members and closing brace
...
}
Full credit to my amazing colleague Giuseppe D'Angelo for making this work!
Armed with our current indentation level we can now increment this and delegate outputting of the members to the derived class:
auto format(auto v, fmt::format_context &ctx) const
-> fmt::format_context::iterator
{
// Output type name
...
// Extract indentation level to use
...
// Output members and closing brace
memberIndent = indentToUse + 4; // Yes it's a magic number
derived.format_members(v, ctx);
// Output closing curly brace
...
}
From the above, we see that we expect a derived class to now also provide another member function called format_members
. This function should output the members that you care about. To make life easier for developers, let's make a couple of functions on our base class that can be used by the derived classes to format a "scalar" value as well as a member that is itself another struct:
void format_scalar(std::string_view label, auto v, fmt::format_context &ctx) const
{
fmt::format_to(ctx.out(), "{:{}}{}: {}\\n", "", memberIndent, label, v);
}
void format_struct(std::string_view label, auto v, fmt::format_context &ctx) const
{
fmt::format_to(ctx.out(), "{:{}}{}: {:{}}\\n", "", memberIndent, label, v, memberIndent);
}
Both of these functions make use of a neat feature in fmt
that allows us to dynamically specify the format specification. In this case we want to be able to specify the indentation used when a derived class calls one of these helpers.
Notice the format specifications in both of these functions include some nested substitutions. When dealing with implicitly ordered substitution placeholders, they are ordered from left to right and then from outside in. The inner placeholders are prefixed by a colon which means the value that gets substituted in will be used to specify the indentation level.
The use of an empty string for the surrounding placeholder just means that fmt
will expand that placeholder to be the specified width - i.e. pad it with spaces. The following color-coded diagrams should hopefully illustrate how the various parts of the format specifications map to the substituted values.
Substitutions for format_scalar
Substitutions for format_struct
One thing of note here is that in the format_struct
function (see Fig. 2), the purple width specifier is used to provide the current member indentation level as the surrounding blue placeholder recurses into the indenting formatter for the child struct. It is the format()
and format_struct()
functions that form the recursive pair to properly indent the output with the level of nesting.
One final piece is just combining the dynamic width trick with outputting a literal closing curly brace to finish off the output for a given struct:
auto format(auto v, fmt::format_context &ctx) const
-> fmt::format_context::iterator
{
// Output type name
...
// Extract indentation level to use
...
// Output members and closing brace
...
// Output closing curly brace
return fmt::format_to(ctx.out(), "{:{}}}}", "", indentToUse);
}
With all of the above elements in place, writing a custom subclass of the indenting formatter for your own type is very easy. Here is the subclass for the Vec2
type used in the motivating example:
template<>
struct fmt::formatter<Vec2> : indenting_formatter<fmt::formatter<Vec2>> {
auto name() const -> std::string_view { return "Vec2"; }
auto format_members(Vec2 v, fmt::format_context &ctx) const
-> fmt::format_context::iterator
{
format_scalar("x", v.x, ctx);
format_scalar("y", v.y, ctx);
return ctx.out();
}
};
All we have to do is:
name()
function to return the type name as we wish to see it.format_members()
function that will typically make use of the format_scalar()
and format_struct()
helpers we built above.There are a few improvements we could make to the above. For example, the indentation is hard-wired to 4 spaces per level but this could be made configurable. Providing another helper on the base class for formatting values wrapped in a std::optional
that could print "<Not Set>" when the optional does not contain a value. Adding support for arrays in whatever format you wish to present them. Even nicer would be if we could generate the derived indenting formatter classes automatically using reflection.
Using the indenting formatter within spdlog
is completely transparent since it uses fmt
under the hood. If you are using spdlog
in your projects, you may also be interested in the KDSpdSetup library that allows you to configure all of your logging from a simple toml file.
We hope that you found this article an interesting exercise in abusing the fmt api slightly to provide a more human readable output of your types. Feel free to take the code and use it as you wish.
About KDAB
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Learn Modern C++
Our hands-on Modern C++ training courses are designed to quickly familiarize newcomers with the language. They also update professional C++ developers on the latest changes in the language and standard library introduced in recent C++ editions.
Learn more