String Interpolation

A detailed guide to string formatting using C++20's std::format(), and C++23's std::print()
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

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()

An introduction to 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

C++23’s 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.

Including Braces in a Template String

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: { }

Positional Arguments

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

Formatting Specifiers

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.

Minimum Width Formatting

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 |

Alignment and Fill Formatting

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

Left, Center, and Right Alignment

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 alignment

For 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 |

Float Precision

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

Using Variables in Format Specifiers

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

Date and Time Formatting (Chrono)

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 Types

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

Summary

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:

  • Mastery of string interpolation in C++ using std::format, enabling dynamic insertion of variables into strings.
  • Understanding the usage of formatting specifiers for precise control over string output.
  • Familiarity with std::print in C++23 for streamlined output formatting.
  • Techniques for including braces and using positional arguments in format strings.
  • Application of std::format with various data types, including custom types and chrono objects for date and time formatting.

Preview of the Next Lesson

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.

Was this lesson useful?

Next Lesson

Overloading the << Operator

Learn how to overload the << operator, so our custom types can stream information directly to the console using std::cout
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 56 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Overloading the << Operator

Learn how to overload the << operator, so our custom types can stream information directly to the console using std::cout
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved