Custom Formatters in C++
Overview
The std::format function enables users to create formatters for
their own custom types. What follows is a guide for writing a custom formatter for a type called MyType,
in a way that covers most scenarios.
The Partial Template Specialization
- Assert that
MyTypeis formattable:static_assert(std::formattable<MyType>)- Not necessary for functionality, but will prevent your code from compiling until the custom formatter has the correct interface.
Create a partial class specialization of std::formatter for your type in the
stdnamespace:/// \brief Enable formatting for MyType objects /// \tparam CharT The character type to use template<class CharT = char> struct std::formatter<MyType, CharT> // (Optional) : std::formatter<AnotherType, CharT> to reuse a parser { /// \brief Parse the format-spec, storing the results in *this so that /// they can be used for controlling how MyType is formatted /// \param parse_ctx An std::basic_format_parse_Context<CharT> /// This contains .begin() and .end() iterators for all characters after /// the : in the format-spec (including the "}"). If the /// format-spec is empty (e.g., "{}") then .begin() == .end() /// \returns std::basic_format_parse_context<CharT>::iterator /// The iterator points to the character that is past the end of the /// last character parsed by the parse function /// If re-using a parser via inheritance, do not include this function here. constexpr auto parse(auto & parse_ctx); /// \brief Writes a string representation of t to the fmt_ctx range /// \param t The type to output. /// (Note: this can also be taken by value instead of const &, if desired). /// \param fmt_ctx An std::basic_format_context<LegacyOutputIterator, CharT> /// Contains an iterator to output characters to. There is no guarantee /// of what type LegacyOutputIterator is, so your code should not /// depend it being a particular iterator type /// \returns std::basic_format_context<>::iterator. The iterator should /// point to one past the last output character (e.g., where the next /// character from whatever else is being added to the string should be inserted) auto format(const MyType & t, auto & fmt_ctx) const; };
- The reason for a partial specialization is to handle both regular and wide characters.
- In our work it's not strictly necessary because we tend not to use wide characters, but might as well.
- The formatter must implement two member functions:
parseandformatparseis for reading the formatting options, as specified in theformat-specformatis for actually creating the string
Implementing Parse
There are three main patterns for implementing parse:
- Re-use a formatter for another type
T. This method is particularly useful if you have, for example, a type that is displayed as twodoublevalues.- In simple cases, you can inherit from the parser in question and omit the
parsemember function entirely. In more complex cases (e.g., your output requires formatting multiple values) it makes sense to use composition and delegate the parsing:
std::formatter<T, CharT> T_formatter; // Member variable constexpr auto parse(auto & parse_ctx) { return T_formatter.parse(parse_ctx); }
- In simple cases, you can inherit from the parser in question and omit the
Don't allow any formatting specifiers (essentially removing any way to control what the output looks like for the user):
constexpr std::basic_format_parse_context<CharT>::iterator parse(std::basic_format_parse_context<CharT> & parse_ctx) { if(parse_ctx.begin() == parse_ctx.end() || *parse_ctx.begin() == "}") { return parse_ctx.begin(); } else { throw std::format_error("Unsupported format string."); } }
- Implement (and document) your own format-specification language
- This involves iterating over the
parse_ctxrange, interpreting characters, and setting members in*thisso the information can be used byformat
- This involves iterating over the
Implementing Format
- The
formatmember function is responsible for writing characters to the iterator provided byfmt_ctx. If you inherited from another formatter (e.g.,
std::formatter<int, CharT>, you can call that formatter's format function directly:auto format(const MyType & t, auto & fmt_ctx) const { return std::formatter<int, charT>.format(t.int_val, fmt_ctx); }
Likewise, if you used composition, you can then use the
.formatmethod of the enclosed formatter:std::formatter<T, CharT> T_formatter; // Member variable auto format(const MyType & t, auto & fmt_ctx) const { return T_formatter.format(t.my_T, fmt_ctx); }
Another useful function is
std::format_to. This function is likestd::formatbut instead of formatting to a string it writes it's output to an Output Iteratorauto format(const MyType & t, auto & fmt_ctx) const { return std::format_to(fmt_ctx.out(), "The value is {}", t.my_val); }
It is very important that if, after calling functions that take the
fmt_ctxas an argument you explicitly update the iterator if you plan on using thefmt_ctxagain. A pattern to ensure the correct behavior is shown below:std::formatter<T, CharT> T_formatter; // Member variable auto format(const MyType & t, auto & fmt_ctx) const { fmt_ctx.advance_to(format_to(fmt_ctx.out(), "some text first")); fmt_ctx.advance_to(T_formatter.format(t.my_T, fmt_ctx)); return fmt_ctx.out(); }
formatter.formattakes thefmt_ctxdirectly whereasformat_totakes the underlying output iterator.- Both
format_toandformatter.formatreturn anOutputIteratorpointing to the next location to write characters - The calls to
fmt_ctx.advance_toensure that the underlyingOutputIteratorinfmt_ctxis always pointing to the next location where characters should be written. - The code therefore maintains
fmt_ctx.out()as the proper location to write the next item in the string