Last Update:
Fun with printing tables with std::format and C++20
Table of Contents
std::format
added in C++20 is a powerful helper for various text formatting tasks. In this blog post, we’ll have fun and print some tables with it. You’ll see the “old” C++17 version and compare it against the C++20 style.
std::format
excercise
As an exercise to learn about std::format
, we can try printing some more advanced structures than just “Hello World”.
For example, if we have a map of data:
constexpr size_t Rows = 5;
const std::map<std::string, std::array<double, Rows>> productToOrders{
{ "apples", {100, 200, 50.5, 30, 10}},
{ "bananas", {80, 10, 100, 120, 70}},
{ "carrots", {130, 75, 25, 64.5, 128}},
{ "tomatoes", {70, 100, 170, 80, 90}}
};
We want to print them in the following table:
apples bananas carrots tomatoes
100.00 80.00 130.00 70.00
200.00 10.00 75.00 100.00
50.50 100.00 25.00 170.00
30.00 120.00 64.50 80.00
10.00 70.00 128.00 90.00
C++17 version
Let’s try the following code from C++17:
// print headers:
for (const auto& [key, val] : productsToOrders)
std::cout << std::setw(10) << key;
std::cout << '\n';
// print values:
for (size_t i = 0; i < NumRows; ++i) {
for (const auto& [key, val] : productsToOrders) {
std::cout << std::setw(10) << std::fixed
<< std::setprecision(2) << val[i];
}
std::cout << '\n';
}
Structured bindings from C++17 help here, but as you can see, sometimes we use only keys
and sometimes only values
.
Improvements into C++20
Okay, we have basic code, but let’s try improving it.
Max column width
The first small thing is that we use a fixed length of fields, so if some text in a column is larger than ten characters, we’ll have some overflow.
We can write the following helper function:
template <typename T>
size_t MaxKeyLength(const std::map<std::string, T>& m) {
size_t maxLen = 0;
for (const auto& [key, val] : m)
if (key.length() > maxLen)
maxLen = key.length();
return maxLen;
}
And use it:
const auto ColLength = MaxKeyLength(productsToOrders) + 2;
Later we can pass it to all std::setw()
functions.
The code is fine, as it handles all maps that have std::string
as the key, and values might be of a different type. Still, it’s better to rely on the more generic standard algorithm.
The first approach might be the following:
template <typename T>
size_t MaxKeyLength(const std::map<std::string, T>& m) {
auto res = std::ranges::max_element(m,
[](const auto& a, const auto& b) {
return a.first.length() < b.first.length();
});
return res->first.length();
}
But we can also use a handy view that returns only keys
:
template <typename T>
size_t MaxKeyLength(const std::map<std::string, T>& m) {
auto res = std::ranges::max_element(std::views::keys(m),
[](const auto& a, const auto& b) {
return a.length() < b.length();
});
return (*res).length();
}
Adding std::format
Let’s now replace output with std::cout
into std::format
calls:
// headers:
for (const auto& name : std::views::keys(productsToOrders))
std::cout << std::format("{:*>{}}", name, ColLength);
std::cout << '\n';
The code uses this special format specifier: "{:>{}}"
:
{:fill-and-align sign width precision type}
The code uses *
as the placeholder character and >
as an alignment (we can also use <
for left or ^
for center). And then, for the width, we use {}
, which points to a length that comes just after name
.
Similarly, for values, we can implement the following loop:
// print values:
for (size_t i = 0; i < NumRows; ++i) {
for (const auto& values : std::views::values(productsToOrders)) {
std::cout << std::format("{:>{}.2f}", values[i], ColLength);
}
std::cout << '\n';
}
When we run this code, we should see the following:
****apples***bananas***carrots**tomatoes
100.00 80.00 130.00 70.00
200.00 10.00 75.00 100.00
50.50 100.00 25.00 170.00
30.00 120.00 64.50 80.00
10.00 70.00 128.00 90.00
You can play with this version @Compiler Explorer
Side note: as of Feb 2023, it looks like all major compilers (MSVC, Clang 17, GCC 13) supportstd::format
and evenstd::chrono
calendar!
Adding dates from std::chrono
While our table looks fine, it lacks some details. What do those numbers in rows mean?
We can add the following:
std::chrono::year_month_day startDate{2023y, month{February}, 20d};
And then add one column:
std::cout << std::format("{:>{}}", "date", ColLength);
And then, print dates before printing the value set:
const auto nextDay = sys_days{ startDate } + days{ i };
std::cout << std::format("{:>{}}", nextDay, ColLength);
Now we have the following table, which is probably easier to read:
Orders:
date apples bananas carrots tomatoes
2023-02-20 100.00 80.00 130.00 70.00
2023-02-21 200.00 10.00 75.00 100.00
2023-02-22 50.50 100.00 25.00 170.00
2023-02-23 30.00 120.00 64.50 80.00
2023-02-24 10.00 70.00 128.00 90.00
As you can see, the code uses a starting date, then adds one day and prints it. Surprisingly we have to convert the date into sys_days
before adding a new day. The initial date format year_month_date
doesn’t have days precision.
Play with code @Compiler Explorer
This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.
Summary
Today’s experiment went from a simple table printing code using C++17 into a fancier version from C++20. The improved code was “nicer” and easier to write and maintain. Additionally, we got working date-time handling capabilities in just a couple of lines of code! Which wasn’t possible until C++20.
Back to you
Have you tried std::format
or maybe {fmt}
?
Share your comments below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: