std::format in C++20

I’m happy to announce that the Text Formatting proposal (std::format) made it into the C++20 Committee Draft at the Cologne meeting, just in time before the feature freeze. This concludes a three-year long design, implementation, and standardization effort.

Here’s a brief history of the proposal aka “what took you so long?”:

2016-08-17:

2016-08-27:

2017-07-10 to 15, Toronto, Canada:

  • presented revision 0 (R0 - as all sensible people we use zero-based revision numbers) of the proposal which received a positive feedback
  • was asked for compile-time processing of format strings, output iterator support, precomputing output size, and benchmark results (as you can see the committee loves to make you do more work - all of these features took months to design, implement, and turn into the paper form since I was working in my spare time, so I skipped one meeting)

2018-03-12 to 17, Jacksonville, USA:

  • presented R1
  • Python-like syntax was approved ({} instead of %)
  • user extensibility of syntax was approved (important for date, time, and other nontrivial formatting, e.g. format("{0:%Y-%m-%d}", t))
  • was asked for a function that formats to a sized range (format_to_n)

2018-06-04 to 09, Rapperswil, Switzerland:

  • presented R2
  • leaving the Unicode support for a separate paper was approved (😌 Relieved Face)
  • nested namespace was dropped (std::format instead of std::fmt::format or similar)
  • the proposal was prioritized for the International Standard as opposed to going to the Library Fundamentals Technical Specification
  • was asked to make the default floating-point format round trip

2018-11-05 to 10, San Diego, USA:

  • presented R4 (R3 was in the post-Rapperswil mailing)
  • type-erased API was approved (std::vformat that doesn’t depend on argument types and gives you compact per-call binary code)
  • specialization-based extension API was approved (std::formatter<T>)
  • using the shortest correctly rounded decimal representation as the default floating-point format (expressed in terms of std::to_chars) was approved
  • the proposal was forwarded to the Library Working Group (LWG) for the wording (the text that goes into the standard) review
  • the wording was reviewed by LWG for the first time during its Saturday “zombie” session

2019-02-18 to 23, Kona, USA:

  • presented R5
  • obscure but important API improvements were approved
  • forwarded back to LWG for C++20
  • survived extensive wording reviews of R7 (where did R6 go?) via multiple teleconferences after the meeting

2019-07-15 to 20, Cologne, Germany:

  • presented R9 (here I was told to stop bumping revisions between mailings, oops)
  • wording review and approval of the final revision R10 that will be in the post-Cologne mailing

Contrary to the usual “design-by-committee” narrative, the standardization process was tremendously beneficial both for the proposal and the {fmt} library, resulting in numerous improvements and here are just a few examples:

  • Iterator support

    The addition of iterator-based std::format_to and std::format_to_n functions makes formatting to arbitrary output targets such as arrays or containers as easy and natural as calling std::copy or std::copy_n:

     std::vector<char> buf;
     std::format_to(std::back_inserter(buf), "{}", 42);
     // buf contains "42"
    

    or

     char buf[10];
     auto result = std::format_to_n(buf, sizeof(buf), "{}", 42);
     // buf contains "42" and result.out points to the end of the output.
     // No dynamic memory allocations.
    
  • Precomputing the output size

    As the name suggests, formatted_size gives you the output size which could be useful for preallocating a buffer:

     auto size = std::formatted_size("{}", 42); // size == 2
     std::vector<char> buf(size);
     std::format_to(buf.data(), "{}", 42);
    
  • Compile-time processing of format string

    Although not exposed through the top-level API yet, the design and the extension API fully support constexpr format string parsing which also applies to user-defined types. This required separation of parser and formatter contexts that hold parsing and formatting state respectively and generally resulted in a cleaner API:

     struct std::formatter<MyType> {
        constexpr auto parse(parse_context& ctx) {
          // Parses the format string range [ctx.begin(), ctx.end()).
          // Can be omitted if inherited from another formatter.
        }
    
        auto format(const MyType& value, format_context& ctx) {
          // Formats value and writes into ctx.out().
          // Can be delegated to another formatter.
        }
     };
    

    This will allow compile-time format string checks and even translating your format strings into formatter objects and eliminating any runtime overhead (which is usually pretty small though).

  • Default representation of floating-point numbers

    Building upon C++17’s std::to_chars and following the example of Python 3’s str.format, std::format provides the shortest correctly-rounded floating point representation as the default. In particular this means, that unlike with printf, there is no precision loss:

     auto s = std::format("{}", M_PI); // s == "3.141592653589793"
     char buf[20];
     sprintf(buf, "%g", M_PI);         // buf contains "3.14159"
    

In addition to the P0645 Text Formatting there were several other formatting papers approved at Cologne:

  1. P1361 Integration of chrono with text formatting written in collaboration with Daniela Engert and Howard E. Hinnant. This paper makes it possible to format all chrono types with std::format and removes now redundant std::chrono::format. Among other things, formatting multiple objects in one go becomes much easier:

    void print_birthday(std::string_view name,
                        const std::chrono::year_month_day& birthday) {
      std::cout << std::format("{0}'s birthday is {1:%Y-%m-%d}.\n", name, birthday);
    }
    

    and you can avoid dynamic memory allocations:

    std::array<char, 100> buf;
    std::format_to_n(buf.data(), buf.size(), "{:%Y-%m-%d}", date);
    
  2. P1652 Printf corner cases in std::format written by Zhihao Yuan. This paper contains a number of critical bug fixes for P0645 Text Formatting. For example, it makes formatting of float round trip (previously float was converted to double as in printf):

    std::string s = std::format("{}", 3.31f); // s == "3.31"
    

    Without the fix s would contain “3.309999942779541”.

  3. P1636 Formatters for library types by Lars Gullik Bjønnes. This paper makes a number of standard library types such as std::complex and std::filesystem::path formattable with std::format, e.g.

    auto s = std::format("{}", 1 + 2i); // s == (1, 2)
    

    It was approved by the Library Evolution Working Group that deals with the design and API, but there was no wording review yet.

Acknowledgements

Thanks to Alberto Barbati, Antony Polukhin, Beman Dawes, Bengt Gustafsson, Daniel Krügler, Daniela Engert, Eric Niebler, Jason McKesson, Jeffrey Yasskin, Joël Lamotte, Lars Gullik Bjønnes, Lee Howes, Louis Dionne, Marshall Clow, Matt Clarke, Michael Park, Sean Middleditch, Sergey Ignatchenko, Thiago Macieira, Titus Winters, Tomasz Kamiński, Zach Laine, Zhihao Yuan, participants of the Library Evolution Working Group and the Library Working Group for their feedback, support, constructive criticism and contributions to the proposal. Special thanks to Howard E. Hinnant who encouraged me to write the proposal and gave useful early advice on how to go about it.

Also thanks to my current and past managers at Facebook for supporting my standardization work and the company for paying for my trips to standards committee meetings.

And last but not least thanks to the many contributors to the {fmt} library for their work.

What’s next?

P0645 has built the foundation for modern, efficient, and extensible text formatting. However, due to time constraints it left unanswered a number of questions that will be addressed by future proposals:

  • Unicode support: we need to provide formatting functions that work with char8_t and other Unicode unit and corresponding string types, correctly handling format specifications expressed in more high-level terms than code units.

  • Compile-time format strings: as mentioned earlier, the extension API is constexpr-ready but we need a convenient top-level API to make use of it.

  • Formatted I/O: it would be nice to have a better integration between formatting and I/O than just passing strings around as in

    std::cout << std::format("The answer is {}.", 42);
    

FAQ

Q: I want to use this now, not wait until C++20 is available and std::format is implemented in my standard library. What should I do?

A: All of this and much more has been implemented in the {fmt} library which works on pretty much anything that considers itself a C++ compiler.

comments powered by Disqus