Formatting User-Defined Types in C++20

Additionally, to the basic types and std::string, you can also format user-defined types in C++20.

So far, I have formatted basic types and std::string. Here are my previous posts:

std::formatter enables it to format user-defined types. You have to specialize the class std::formatter for the user-defined type. In particular, you must implement the member functions parse, and format.

  • parse:  This function parses the format string and throws a std::format_error in case of an error.

The function parse should be constexpr to enable compile-time parsing. It accepts a parse context (std::format_parse_context) and should return the last character for the format specifier (the closing }). When you don’t use a format specifier, this is also the first character of the format specifier.

The following lines show a few examples of the first character of the format specifier:

"{}"          // context.begin() points to }
"{:}"         // context.begin() points to }
"{0:d}"       // context.begin() points to d} 
"{:5.3f}"     // context.begin() points to: 5.3f}
"{:x}"        // context.begin() points to x} 
"{0} {0}"     // context.begin() points to: "} {0}"

context.begin() points to the first character of the format specifier and context.end() to the last character of the entire format string. When you provide a format specifier, you have to parse all between context.begin() and context.end() and return the position of the closing }.

  • format: This function should be const. It gets the value val, and the format context context. format formats the value val, and writes it according to the parsed format to context.out(). The context.out() return value can be directly fed into std::format_to. std::format_to has to return the new position for further output. It returns an iterator that represents the end of the output.

Let me apply the theory and start with the first example.

A Formatter for a Single Value

// formatSingleValue.cpp

#include <format>
#include <iostream>

class SingleValue {                                          // (1)
 public: 
   SingleValue() = default;
   explicit SingleValue(int s): singleValue{s} {}
   int getValue() const {                                    // (2)
     return singleValue;
   }
 private:
   int singleValue{};
};

template<>                                                   // (3)
struct std::formatter<SingleValue> {
  constexpr auto parse(std::format_parse_context& context) { // (4)
    return context.begin();
  }
  auto format(const SingleValue& sVal, std::format_context& context) const {  // (5)
    return std::format_to(context.out(), "{}", sVal.getValue());
  }
};

int main() {

  std::cout << '\n'; 

  SingleValue sVal0;
  SingleValue sVal2020{2020};
  SingleValue sVal2023{2023};

  std::cout << std::format("Single Value: {} {} {}\n", sVal0, sVal2020, sVal2023);
  std::cout << std::format("Single Value: {1} {1} {1}\n", sVal0, sVal2020, sVal2023);
  std::cout << std::format("Single Value: {2} {1} {0}\n", sVal0, sVal2020, sVal2023);

  std::cout << '\n';

}

SingleValue (line 1) is a class having only one value. The member function getValue (line 2) returns this value. I specialize std::formatter (line 3) on SingleValue. This specialization has the member functions parse (line 4) and format (line 5). parse returns the end of the format specification. The end of the format specification is the closing }. format formats the value, and context.out creates an object passed to std::format_to. format returns the new position for further output.

Executing this program gives the expected result:

This formatter has a severe drawback. It does not support a format specifier. Let me improve that.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    A Formatter Supporting a Format Specifier

    Implementing a formatter for a user-defined type is pretty straightforward when you base your formatter on a standard formatter. Basing a user-defined formatter on a standard formatter can be done in two ways: delegation and inheritance.

    Delegation

    The following formatter delegates its job to a standard formatter.

    // formatSingleValueDelegation.cpp
    
    #include <format>
    #include <iostream>
    
    class SingleValue {
     public: 
        SingleValue() = default;
        explicit SingleValue(int s): singleValue{s} {}
        int getValue() const {
            return singleValue;
        }
     private:
        int singleValue{};
    };
    
    template<>                                                         // (1)
    struct std::formatter<SingleValue> {
    
        std::formatter<int> formatter;                                 // (2)
    
        constexpr auto parse(std::format_parse_context& context) {
            return formatter.parse(context);                           // (3)
        }
    
        auto format(const SingleValue& singleValue, std::format_context& context) const {
            return formatter.format(singleValue.getValue(), context);  // (4)
        }
    
    }; 
    
    int main() {
    
        std::cout << '\n'; 
    
        SingleValue singleValue0;
        SingleValue singleValue2020{2020};
        SingleValue singleValue2023{2023};
    
        std::cout << std::format("{:*<10}", singleValue0) << '\n';
        std::cout << std::format("{:*^10}", singleValue2020) << '\n';
        std::cout << std::format("{:*>10}", singleValue2023) << '\n';
    
        std::cout << '\n';
    
    }
    

    std::formatter<SingleValue> (line 1) has a standard formatter for int: std::formatter<int> formatter (line 2). I delegate the parsing job to the formatter (line 3). Accordingly, the formatting job format is also delegated to the formatter (line 4).

    The program’s output shows that the formatter supports fill characters and alignment.

    Inheritance

    Thanks to inheritance, implementing the formatter for the user-defined type SingleValue is a piece of cake.

    // formatSingleValueInheritance.cpp
    
    #include <format>
    #include <iostream>
    
    class SingleValue {
     public: 
        SingleValue() = default;
        explicit SingleValue(int s): singleValue{s} {}
        int getValue() const {
            return singleValue;
        }
     private:
        int singleValue{};
    };
    
    template<>
    struct std::formatter<SingleValue> : std::formatter<int> {             // (1)
      auto format(const SingleValue& singleValue, std::format_context& context) const {
        return std::formatter<int>::format(singleValue.getValue(), context);
      }
    };
    
    int main() {
    
        std::cout << '\n'; 
    
        SingleValue singleValue0;
        SingleValue singleValue2020{2020};
        SingleValue singleValue2023{2023};
    
        std::cout << std::format("{:*<10}", singleValue0) << '\n';
        std::cout << std::format("{:*^10}", singleValue2020) << '\n';
        std::cout << std::format("{:*>10}", singleValue2023) << '\n';
    
        std::cout << '\n';
    
    }
    

    I derive std::formatter<SingleValue> from std::formatter<int> (line 1). Only the format function must be implemented. The output of this program is identical to the output of the previous program formatSingleValueDelegation.cpp.

    Delegating to a standard formatter or inheriting from one is a straightforward way to implement a user-defined formatter. This strategy only works for user-defined types having one value.

    What’s Next?

    In my next blog, I will implement a formatter for a user-defined type having more than one value.

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, Charles-Jianye Chen, and Keith Jeffery.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

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

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,