std::format in C++20

std::format in C++20

This is a cross-post from www.ModernesCpp.com.

Today, I'm happy to present Peter Gottschling's guest post to the new formatting library in C++20: std::format. Thanks to std::format, text formatting becomes in C++20 as easy as in Python.

Peter is the author of the must-read book "Discovering Modern C++" for professional C++ developers.

New Formatting

Traditional stream formatting requires a fair amount of typing. Format strings in printf and alike are clearly more expressive and allow us to declare with few symbols what we've written with multiple I/O manipulators before. 

Nonetheless, we advise against using printf. For two reasons: it can't be used with user types and it is not type-safe. The format string is parsed at run time and the following arguments are treated with an obscure macro mechanism. If the arguments don't match the format string, the behavior is undefined and can cause program crashes. For instance, a string is passed as a pointer and from the pointed address on the bytes are read and printed as char until a binary 0 is found in memory. If we accidentally try printing an int as a string, the int value is misinterpreted as an address from which a sequence of char shall be printed. This will result in either absolute nonsensical output or (more likely) in a memory error if the address is inaccessible. We have to admit that recent compilers parse format strings (when known at compile time) and warn about argument mismatches.

The new format library {By the time of writing, no compiler supported the library and the examples were implemented with its prototype version: the fmt library. The format library combines the expressibility of the format string with the type safety and the user-extensibility of stream I/O and adds the opportunity to reorder the arguments in the output.

Integrals

Instead of a formal specification, we port some printf examples from cppreference.com to the new format:

print("Decimal:\t{} {} {:06} {} {:0} {:+} {:d}\n", 1, 2, 3, 0, 0, 4, -1);

print("Hexadecimal:\t{:x} {:x} {:X} {:#x}\n", 5, 10, 10, 6);

print("Octal:\t\t{:o} {:#o} {:#o}\n", 10, 10, 4);

print("Binary:\t\t{:b} {:#b} {:#b}\n", 10, 10, 4);

 This snippet prints:

Decimal:        1 2 000003 0 0 +4 -1

Hexadecimal:    5 a A 0x6

Octal:          12 012 04

Binary:         1010 0b1010 0b100

The first two numbers were just printed without giving any format information. The same output is generated when we ask for a decimal number with the format specifier :d. The third number shall be printed (minimally) 6~characters wide and filled with 0s. The specifier + allows us to force printing the sign for all numbers. printf allows for specifying unsigned output of numbers. That leads to incorrect large numbers when the value to print is negative. The format library refrains from user declarations of unsigned output since this information is already contained in the type of the according argument. If somebody feels the urge to print a negative value as a large positive one, he must convert it explicitly.

The second line demonstrates that we can print values hexadecimally--both with lower and upper case for the digits larger than 9. The specifier # generates the prefix 0x used in hexadecimal literals.

Likewise, we can print the values as octals and binaries, optionally with the according literal prefix.

Floating Point Numbers

With floating-point numbers we have more formatting options:

print("Default:\t{} {:g} {:g}\n", 1.5, 1.5, 1e20);

print("Rounding:\t{:f} {:.0f} {:.22f}\n", 1.5, 1.5, 1.3);

print("Padding:\t{:05.2f} {:.2f} {:5.2f}\n", 1.5, 1.5, 1.5);

print("Scientific:\t{:E} {:e}\n", 1.5, 1.5);

print("Hexadecimal:\t{:a} {:A}\n\n", 1.5, 1.3);

Then we get:

Default:        1.5 1.5 1e+20

Rounding:       1.500000 2 1.3000000000000000444089

Padding:        01.50 1.50  1.50

Scientific:     1.500000E+00 1.500000e+00

Hexadecimal:    0x1.8p+0 0X1.4CCCCCCCCCCCDP+0

With empty braces or only containing a colon, we get the default output. This corresponds to the format specifier :g and yields the same output as streams without the manipulators. The number of fractional digits can be given between a dot and the format specifier f. Then the value is rounded to that precision. If the requested number is larger than what is representable by the value's type, the last digits aren't very meaningful. A digit in front of the dot specifies the (minimal) width of the output. As with integers, we can request leading 0s. Floating-point numbers can be printed in the scientific notation with either upper or lower case e to start the exponential part. The hexadecimal output can be used to initialize a variable in another program with precisely the same bits.

Redirecting Output

The output can be redirected to any other std::ostream (Requires including ostream.h with the fmt library.):

print(std::cerr, "System error code = {}\n", 7);

ofstream error_file("error_file.txt");

print(error_file, "System error code = {}\n", 7);

Reordering Arguments and Name them

In contrast to printf, arguments can now be reordered:

print("I'd rather be {1} than {0}.\n", "right", "happy");

 In addition to referring the arguments by their positions, we can give them names:

print("Hello, {name}! The answer is {number}. Goodbye, {name}.\n",
      arg("name", name), arg("number", number));

Or more concisely:

print("Hello, {name}! The answer is {number}. Goodbye, {name}.\n",
      "name"_a=name, "number"_a=number);

The example also demonstrates that we can print an argument multiple times.

Reordering arguments is very important in multi-lingual software to provide a natural phrasing.

Now, we want to print the average of two values in five languages:

void print_average(float v1, float v2, int language)

{   

    using namespace fmt;

    string formats[]= {"The average of {v1} and {v2} is {result}.\n",

                       "{result:.6f} ist der Durchschnitt von {v1} und {v2}.\n",

                       "La moyenne de {v1} et {v2} est {result}.\n",

                       "El promedio de {v1} y {v2} es {result}.\n",

                       "{result} corrisponde alla media di {v1} e {v2}.\n"};

    print (formats[language], "v1"_a= v1, "v2"_a= v2, "result"_a= (v1+v2)/2.0f);

}   

Of course, the German version is the most pedantic one, requesting 6 decimal digits no matter what:

The average of 3.5 and 7.3 is 5.4.

5.400000 ist der Durchschnitt von 3.5 und 7.3.

La moyenne de 3.5 et 7.3 est 5.4.

El promedio de 3.5 y 7.3 es 5.4.

5.4 corrisponde alla media di 3.5 e 7.3.

Admittedly, this example would have worked without reordering the arguments but it nicely demonstrates the important possibility to separate the text and the formatting from the values. To store formatted text in a string we don't need a stringstream any longer but can do it directly with the function format.

What's next?

In the next post, Peter continues his introduction to std::format. He writes about user-defined formatting.

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Jon Hess, Christian Wittenhorst, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Lakshman, Kuchlong Kuchlong, Avi Kohn, Serhy Pyton, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, Kai, and Sudhakar Balagurusamy.

Thanks in particular to Kuma Dev.

 

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

Contact Me

Modernes C++


To view or add a comment, sign in

More articles by Rainer Grimm

  • Charity run for ALS

    Tomorrow, on the 28th, there will be a charity run for ALS in Rottenburg. The course is exactly 1 km long.

    2 Comments
  • Small Safety Improvements in the C++ 26 Core Language

    Safety is an important concern in C++26. Contracts are probably the most important feature for safety.

    1 Comment
  • My ALS Journey (30/n): Cippi at the CppCon

    This week was very exciting for Cippi. She visited CppCon in Aurora, near Denver.

    2 Comments
  • Contracts: Evaluation Semantic

    After briefly presenting the details of contracts in my last article, “Contracts: A Deep Dive“, I would like to take a…

  • My ALS Journey (29/n): I feel Good

    I often receive messages asking about my health and wishing me well. I am very happy to receive these messages and just…

    5 Comments
  • Contracts: A Deep Dive

    August 25, 2025/in C++26/by Rainer GrimmI already introduced contracts in the article “Contracts in C++26”. In this…

  • My ALS Journey (28/n): Bureaucracy – The German Disease

    Today I want to write about a sad topic. Bureaucracy in the German healthcare system is becoming increasingly absurd.

    2 Comments
  • Data-Parallel Types: Algorithms

    The data-parallel types library has four special algorithms for SIMD vectors. The four special algorithms are min, max,…

  • My ALS Journey (27/n): An Emergency Call

    Firstly, I would like to say that I am doing very well and have made a full recovery from my incident. However, I would…

    8 Comments
  • Data-Parallel Types: Reduction

    In this article, I will discuss reduction and mask reduction for data-parallel types. Reduction A reduction reduces the…

Others also viewed

Explore content categories