3636. formatter<T>::format should be const-qualified

Section: 28.5.6.1 [formatter.requirements] Status: C++23 Submitter: Arthur O'Dwyer Opened: 2021-11-11 Last modified: 2023-11-22

Priority: 1

View all other issues in [formatter.requirements].

View all issues with C++23 status.

Discussion:

In libc++ review, we've noticed that we don't understand the implications of 28.5.6.1 [formatter.requirements] bullet 3.1 and Table [tab:formatter.basic]: (emphasize mine):

(3.1) — f is a value of type F,

[…]

Table 70: BasicFormatter requirements [tab:formatter.basic]

[…]

f.parse(pc) [must compile] […]

f.format(u, fc) [must compile] […]

According to Victor Zverovich, his intent was that f.parse(pc) should modify the state of f, but f.format(u, fc) should merely read f's state to support format string compilation where formatter objects are immutable and therefore the format function must be const-qualified.

That is, a typical formatter should look something like this (modulo errors introduced by me):

struct WidgetFormatter {
  auto parse(std::format_parse_context&) -> std::format_parse_context::iterator;
  auto format(const Widget&, std::format_context&) const -> std::format_context::iterator;
};

However, this is not reflected in the wording, which treats parse and format symmetrically. Also, there is at least one example that shows a non-const format method:

template<> struct std::formatter<color> : std::formatter<const char*> {
  auto format(color c, format_context& ctx) {
    return formatter<const char*>::format(color_names[c], ctx);
  }
};

Victor writes:

Maybe we should […] open an LWG issue clarifying that all standard formatters have a const format function.

I'd like to be even more assertive: Let's open an LWG issue clarifying that all formatters must have a const format function!

[2022-01-30; Reflector poll]

Set priority to 1 after reflector poll.

[2022-08-24 Approved unanimously in LWG telecon.]

[2022-11-12 Approved at November 2022 meeting in Kona. Status changed: Voting → WP.]

Proposed resolution:

This wording is relative to N4901.

  1. Modify 28.5.6.1 [formatter.requirements] as indicated:

    [Drafting note: It might also be reasonable to do a drive-by clarification that when the Table 70 says "Stores the parsed format specifiers in *this," what it actually means is "Stores the parsed format specifiers in g." (But I don't think anyone's seriously confused by that wording.)

    -3- Given character type charT, output iterator type Out, and formatting argument type T, in Table 70 and Table 71:

    1. (3.1) — f is a value of type (possibly const) F,

    2. (3.?) — g is an lvalue of type F,

    3. (3.2) — u is an lvalue of type T,

    4. (3.3) — t is a value of a type convertible to (possibly const) T,

    5. […]

    […]

    Table 70: Formatter requirements [tab:formatter]
    Expression Return type Requirement
    fg.parse(pc) PC::iterator […]
    Stores the parsed format specifiers in *this and returns an iterator past the end of the parsed range.
  2. Modify 28.5.6.4 [format.formatter.spec] as indicated:

    -6- An enabled specialization formatter<T, charT> meets the BasicFormatter requirements (28.5.6.1 [formatter.requirements]).

    [Example 1:

    #include <format>
    
    enum color { red, green, blue };
    const char* color_names[] = { "red", "green", "blue" };
    
    template<> struct std::formatter<color> : std::formatter<const char*> {
      auto format(color c, format_context& ctx) const {
        return formatter<const char*>::format(color_names[c], ctx);
      }
    };
    
    […]
    

    end example]

  3. Modify 28.5.6.7 [format.context] as indicated:

    void advance_to(iterator it);
    

    -8- Effects: Equivalent to: out_ = std::move(it);

    [Example 1:

    struct S { int value; };
    
    template<> struct std::formatter<S> {
      size_t width_arg_id = 0;
      
      // Parses a width argument id in the format { digit }.
      constexpr auto parse(format_parse_context& ctx) {
        […]
      }
      
      // Formats an S with width given by the argument width_arg_id.
      auto format(S s, format_context& ctx) const {
        int width = visit_format_arg([](auto value) -> int {
          if constexpr (!is_integral_v<decltype(value)>)
            throw format_error("width is not integral");
          else if (value < 0 || value > numeric_limits<int>::max())
            throw format_error("invalid width");
          else
            return value;
          }, ctx.arg(width_arg_id));
        return format_to(ctx.out(), "{0:x<{1}}", s.value, width);
      }
    };
    
    […]
    

    end example]

  4. Modify 30.12 [time.format] as indicated:

    template<class Duration, class charT>
    struct formatter<chrono::local-time-format-t<Duration>, charT>;
    

    -15- Let f be […]

    -16- Remarks: […]

    template<class Duration, class TimeZonePtr, class charT>
    struct formatter<chrono::zoned_time<Duration, TimeZonePtr>, charT>
      : formatter<chrono::local-time-format-t<Duration>, charT> {
      template<class FormatContext>
        typename FormatContext::iterator
          format(const chrono::zoned_time<Duration, TimeZonePtr>& tp, FormatContext& ctx) const;
    };
    
    template<class FormatContext>
      typename FormatContext::iterator
        format(const chrono::zoned_time<Duration, TimeZonePtr>& tp, FormatContext& ctx) const;
    

    -17- Effects: Equivalent to:

    sys_info info = tp.get_info();
    return formatter<chrono::local-time-format-t<Duration>, charT>::
             format({tp.get_local_time(), &info.abbrev, &info.offset}, ctx);