Formatting User-Defined Types in C++20

Formatting User-Defined Types in C++20

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

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.

?


Modernes C++ Mentoring

Be part of my mentoring programs:

Do you want to stay informed: Subscribe.

?

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. I would like to improve that.

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, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, 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, and Philipp Lenk.

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


Farid Mehrabi

Modern C++ programming specialist

9 个月

When is the input method going to evolve? After `std::print`, we're gonna need a `std::scan` as well. Is there any vision on moving toward s deprecation of `<iostream>`?

要查看或添加评论,请登录

社区洞察

其他会员也浏览了