std::format()
, and C++23's std::print()
By now, we’re likely very comfortable formatting output, by chaining the <<
operator.
We can combine strings with other variables to get the exact text output we want:
std::cout << "Health: " << Health << '\n';
There is another, more flexible approach to this, called string interpolation.
With string interpolation, we create a string containing specific character sequences that denote placeholders. A string that includes placeholders to be used for interpolation is sometimes referred to as a format string.
Once we have our format string, we separately provide variables to be inserted into it, replacing the placeholders.
As of C++20, the main way of achieving this within the standard library is by using std::format()
std::format()
std::format
is available within the <format>
header.
In its most basic usage, we provide it with a format string as the first argument. That string should contain a placeholder marked {}
. We then provide a value to insert into that placeholder as a second argument:
#include <format>
int main(){
std::format("I have {} apples", 5);
}
std::format
returns a std::string
, which we can store like any other variable, or stream to the console directly:
#include <format>
#include <iostream>
int main(){
std::string Apples{
std::format("I have {} apples", 5)};
std::cout
<< Apples
<< std::format(" and {} bananas", 2);
}
I have 5 apples and 2 bananas
Our format string can have any number of placeholders, which we can populate with values provided as additional arguments:
#include <format>
#include <iostream>
int main(){
std::string Fruit{
std::format(
"{} apples, {} bananas, and {} pears",
5, 2, 8)};
std::cout << Fruit;
}
5 apples, 2 bananas, and 8 pears
Naturally, our values are not restricted to literal integers. We can provide any expression that results in a value of a type that is supported by std::format
:
#include <format>
#include <iostream>
float CountBananas(){ return 2.5; }
int main(){
int Apples{5};
std::string Fruit{
std::format(
"{} apples, {} bananas, and {} pears",
Apples, CountBananas(), "zero"
)};
std::cout << Fruit;
}
5 apples, 2.5 bananas, and zero pears
std::print()
C++23 introduced std::print()
, which combines the effect of std::format()
and directly sends the output to a stream, such as std::cout
.
As such, code like this:
#include <format>
#include <iostream>
int main() {
std::cout << std::format("{} apples", 5);
}
Can be written as:
#include <print>
int main(){
std::print("{} apples", 5);
}
We’ll stick to using std::format()
in the rest of the examples because it’s much more commonly used. But, when working on a project where std::print()
is supported, it’s worth being aware that it is an option.
If we want to include literal {
and }
characters in our string when using std::format()
, we duplicate them.
For example, if we want a single {
to be output, our template string should contain {{
:
#include <format>
#include <iostream>
int main(){
std::string Fruit{
std::format(
"{} apples and some braces: {{ }}", 5
)};
std::cout << Fruit;
}
5 apples and some braces: { }
As we’ve seen, each placeholder marked {}
corresponds sequentially to our variable values. The first {}
is replaced with the first value, the second {}
is replaced by the second value, and so on.
We can control this by inserting integers between the braces, representing which value to use. These positions are zero-based, which means we start counting from 0.
That means the first value has an index of 0
, the second has an index of 1
, and so on. This is a fairly common convention in programming, and we’ll see more examples of it later.
Providing these positional arguments allows us to control the order in which our values are used to generate the final string:
#include <format>
#include <iostream>
int main(){
std::string Name{
std::format("{1}, {0}", "James", "Bond"
)};
std::cout << Name;
}
Bond, James
It also allows us to use the same value multiple times:
#include <format>
#include <iostream>
int main(){
std::string Name{
std::format("{1}, {0} {1}",
"James", "Bond")};
std::cout << Name;
}
Bond, James Bond
When we’re inserting values into our format strings, we often can specify additional options, controlling how those values get inserted.
Within the placeholder, we pass options after a colon character :
For example, numeric types support the +
specifier, which will prefix a +
to the value, if the value is positive. To use it, our placeholder would look like {:+}
as shown below:
#include <format>
#include <iostream>
int main(){
std::string Number{
std::format("{:+}", 10)};
std::cout << Number;
}
+10
A placeholder’s positional argument goes before the :
so, when we’re using both a positional argument and a format specifier, our placeholder would look like this:
#include <format>
#include <iostream>
int main(){
std::string Number{
std::format("{0:+}, {1:+}", 10, -42)};
std::cout << Number;
}
+10, -42
Which formatters are available depends on the type of the variable we’re inserting into our string.
We’ll cover the most useful specifiers of the built-in types, and how to use them, in the rest of this lesson. A complete list of all options is available from one of the standard library references, such as cppreference.com.
Passing a simple, positive integer as the format specifier sets the minimum width of that value within the string.
For example, inserting an integer like 3
into a string only requires one space. But, if we set a minimum width of 6
, additional space will be added to ensure our variable uses at least 6 characters:
#include <format>
#include <iostream>
int main(){
std::string Fruit{
std::format("I have {:6} apples", 3)};
std::cout << Fruit;
}
I have 3 apples
The minimum width and alignment specifiers (covered later) are mostly useful when we’re outputting multiple lines of text.
For example, if we’re trying to create a table, we will want our columns to be vertically aligned. Setting minimum widths can help us achieve that.
Below, we use these specifiers to create a table of 3 columns, with widths of 8
, 5
, and 6
respectively:
#include <format>
#include <iostream>
int main(){
std::cout
<< std::format(
"{:8} | {:5} | {:6} |",
"Item", "Sales", "Profit")
<< std::format(
"\n{:8} | {:5} | {:6} |",
"Apples", 53, 8.21)
<< std::format(
"\n{:8} | {:5} | {:6} |",
"Bananas", 194, 33.89)
<< std::format(
"\n{:8} | {:5} | {:6} |",
"Pears", 213, 106.35);
}
Item | Sales | Profit |
Apples | 53 | 8.21 |
Bananas | 194 | 33.89 |
Pears | 213 | 106.35 |
When our variable is an integer, we can prefix our minimum width value with a 0
. This has the effect that any additional space inserted is filled by leading zeroes:
#include <format>
#include <iostream>
int main(){
std::string Fruit{
std::format("I have {:04} apples", 3)};
std::cout << Fruit;
}
I have 0003 apples
We can prefix our width specifier with an additional character, specifying how our value should be aligned within that width.
<
denotes left alignment^
denotes center alignment>
denotes right alignmentFor example, if we wanted our field width to be 12 spaces, and for our value to be centered within that space, our placeholder would be {:^12}
#include <format>
#include <iostream>
int main(){
std::string Alignment{
std::format("| {:^12} |",
"Center")};
std::cout << Alignment;
}
| Center |
The following example shows the effect of the three alignment options:
#include <format>
#include <iostream>
int main(){
std::string Alignment{
std::format(
"| {:<12} |\n| {:^12} |\n| {:>12} |",
"Left", "Center", "Right"
)};
std::cout << Alignment;
}
| Left |
| Center |
| Right |
We can additionally place a character before our alignment arrow, specifying what we want any additional space filled with. Below, we fill the space with -
characters:
#include <format>
#include <iostream>
int main(){
std::string Alignment{
std::format(
"| {:-<12} |\n| {:-^12} |\n| {:->12} |",
"Left", "Center", "Right")};
std::cout << Alignment;
}
| Left-------- |
| ---Center--- |
| -------Right |
The most common formatting requirement when working with floating point numbers is to set their level of precision.
This is done using a .
followed by the number of significant figures we want to be included in the resulting string:
#include <format>
#include <iostream>
int main(){
float Pi{3.141592};
std::cout
<< std::format("Pi: {:.1}", Pi)
<< std::format("\nPi: {:.3}", Pi)
<< std::format("\nPi: {:.5}", Pi);
}
Pi: 3
Pi: 3.14
Pi: 3.1416
Below, we’ve combined this with a width argument (8
) alongside right alignment using >
, with any surplus space filled with -
characters:
#include <format>
#include <iostream>
int main(){
float Pi{3.141592};
std::cout
<< std::format("Pi: {:->8.1}", Pi)
<< std::format("\nPi: {:->8.3}", Pi)
<< std::format("\nPi: {:->8.5}", Pi);
}
Pi: -------3
Pi: ----3.14
Pi: --3.1416
When we have a format specifier within a placeholder, we can sometimes additionally nest more placeholders within it.
This allows our variable’s format specifiers to themselves use variables.
Below, we interpolate the argument at index 0
(that is, the integer 3
) into our string, setting the minimum width to the argument at index 1
(that is, the integer 4
):
#include <format>
#include <iostream>
int main(){
std::string Number{
std::format("I have {0:{1}} apples", 3, 4)};
std::cout << Number;
}
I have 3 apples
Positional arguments are optional here. When omitted, the positions will be inferred based on the position of the opening {
of each placeholder:
#include <format>
#include <iostream>
int main(){
std::string Number{
std::format("I have {:{}} apples", 3, 4)};
std::cout << Number;
}
I have 3 apples
Earlier in the course, we covered the date and time utilities within the C++ standard library, called chrono
Most of the Chrono types include support for std::format()
:
#include <iostream>
#include <chrono>
int main(){
using namespace std::chrono;
time_point tp{system_clock::now()};
std::cout << std::format("{}", tp);
}
2023-12-05 14:59:34.8811286
They additionally have a large range of options for format specifiers, allowing us to customize date strings in highly elaborate ways:
#include <chrono>
#include <iostream>
int main(){
using namespace std::chrono;
time_point tp{system_clock::now()};
std::cout
<< "Today is "
<< std::format(
"{:%A, %e %B\nThe time is %I:%M %p}",
tp
);
}
Today is Tuesday, 12 December
The time is 02:42 AM
A full range of all of the supported syntax is available from a standard library reference, such as cppreference.com.
We cover the most commonly used options below:
#include <chrono>
#include <print>
int main(){
using std::print;
auto T {std::chrono::system_clock::now()};
print("{:%c}", T);
print("\n{:%R}", T);
print("\n{:%r}", T);
print("\n\nYear and Month");
print("\nYear (4 digits): {:%Y}", T);
print("\nYear (2 digits): {:%y}", T);
print("\nMonth: {:%B}", T);
print("\nMonth (3 chars): {:%b}", T);
print("\nMonth (01-12): {:%m}", T);
print("\n\nDay");
print("\nDay of Month: {:%d}", T);
print("\nDay (leading 0): {:%d}", T);
print("\nDay of week: {:%A}", T);
print("\nDay of week (3 chars): {:%a}", T);
print("\n0 - 6 (0 = Sun, 6 = Sat): {:%u}", T);
print("\n1 - 7 (1 = Mon, 7 = Sun): {:%w}", T);
print("\n\nTime");
print("\nHours (00-23): {:%H}", T);
print("\nHours (1-12): {:%I}", T);
print("\nAM / PM: {:%p}", T);
print("\nMinutes (0-59): {:%M}", T);
print("\nSeconds: (0-59): {:%OS}", T);
print("\nTimezone: {:%Z}", T);
}
12/05/23 14:33:46
14:33
02:33:46 PM
Year and Month
Year (4 digits): 2023
Year (2 digits): 23
Month: December
Month (3 chars): Dec
Month (01-12): 12
Day
Day of Month: 5
Day (leading 0): 05
Day of week: Tuesday
Day of week (3 chars): Tue
0 - 6 (0 = Sun, 6 = Sat): 2
1 - 7 (1 = Mon, 7 = Sun): 2
Time
Hours (00-23): 14
Hours (1-12): 02
AM / PM: PM
Minutes (0-59): 33
Seconds: (0-59): 46
Timezone: UTC
std::format()
and std::print()
with Custom TypesThe mechanisms that underpin std::format()
and std::print()
are designed such that any type can implement support for them. We saw an example of this with chrono time_point
objects, but this is not restricted to standard library types. Our user-defined types can also implement support for std::format
and std::print
.
For example, we could enable a user-defined Character
type to be compatible with standard library formatting, including format specifiers.
Anyone using our class could then easily interpolate a Character
object’s data members into their string, perhaps using code like this:
int main(){
Character Player;
std::format(
"I have {0:H} health and {0:M} mana", Player
);
}
Enabling this requires knowledge of templates - an advanced C++ concept that we cover in the next course.
In this lesson, we explored the powerful features of std::format
and std::print
in C++20, delving into string interpolation, formatting specifiers, and custom type support.
These tools enhance the flexibility and readability of string manipulation. The key topics we covered include:
std::format
, enabling dynamic insertion of variables into strings.std::print
in C++23 for streamlined output formatting.std::format
with various data types, including custom types and chrono
objects for date and time formatting.In our upcoming lesson, we'll revisit operator overloading, specifically focusing on overloading the <<
operator. This will enable us to seamlessly stream custom types to std::cout
.
A detailed guide to string formatting using C++20's std::format()
, and C++23's std::print()
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way