4107. Map formatter may conflict with user-defined specializations of pair/tuple formatters

Section: 28.5.7.4 [format.range.fmtmap] Status: New Submitter: Victor Zverovich Opened: 2024-05-18 Last modified: 2024-05-19

Priority: Not Prioritized

View all issues with New status.

Discussion:

Consider the following example:

#include <format>
#include <map>
#include <print>

struct x {};

template<typename K>
struct std::formatter<std::pair<K, x>> : std::formatter<std::string_view> {
  auto format(const std::pair<K, x>& p, auto& ctx) const {
    return std::format_to(ctx.out(), "x/x");
  }
};

int main() {
  std::print("{}", std::map<x, x>());
}

It doesn't compile because the formatter for maps requires the element formatter to have set_brackets and set_separator (28.5.7.4 [format.range.fmtmap]):

underlying_.underlying().set_brackets({}, {});
underlying_.underlying().set_separator(STATICALLY-WIDEN<charT>(": "));

The specialization std::formatter<std::pair<K, x>> itself is allowed according to 16.4.5.2.1 [namespace.std]:

Unless explicitly prohibited, a program may add a template specialization for any standard library class template to namespace std provided that

but it's unclear what exactly the part "the specialization meets the standard library requirements for the original template" means for this formatter. Does it mean that the specialization must provide set_brackets and set_separator and does the output have to be consistent with the main template? The latter would render the specialization useless. On the other hand if users are allowed to customize pair and tuple formatting the current specification of the map formatter is broken.

The correct resolution appears to be to not restrict user-defined formatter specializations of pairs and tuples, and make map be responsible for its own structural formatting rather than delegating part of it to other formatters in an arbitrary way. This resolution has been applied to {fmt}'s implementation of range formatting to address #3685.

Proposed resolution:

This wording is relative to N4981.

  1. Modify 28.5.8.3 [format.args] as indicated:

    namespace std {
      template<ranges::input_range R, class charT>
      struct range-default-formatter<range_format::map, R, charT> {
      private:
        using maybe-const-map = fmt-maybe-const<R, charT>;            // exposition only
        using element-type =                                          // exposition only
          remove_cvref_t<ranges::range_reference_t<maybe-const-map>>;
        range_formatter<element-type, charT> underlying_;             // exposition only
        using key-type = tuple_element_t<0, element-type>;            // exposition only
        using value-type = tuple_element_t<1, element-type>;          // exposition only
        formatter<key-type, charT> key-formatter_;                    // exposition only
        formatter<value-type, charT> value-formatter_;                // exposition only
    
      public:
        constexpr range-default-formatter();
        
        template<class ParseContext>
          constexpr typename ParseContext::iterator
            parse(ParseContext& ctx);
            
        template<class FormatContext>
          typename FormatContext::iterator
            format(maybe-const-map& r, FormatContext& ctx) const;
      };
    }
    
    constexpr range-default-formatter();
    

    -1- Mandates: Either:

    1. (1.1) — element-type is a specialization of pair, or

    2. (1.2) — element-type is a specialization of tuple and tuple_size_v<element-type> == 2.

    -2- Effects: Equivalent to:

    underlying_.set_brackets(STATICALLY-WIDEN<charT>("{"), STATICALLY-WIDEN<charT>("}"));
    underlying_.underlying().set_brackets({}, {});
    underlying_.underlying().set_separator(STATICALLY-WIDEN<charT>(": "));
    
    template<class ParseContext>
      constexpr typename ParseContext::iterator
        parse(ParseContext& ctx);
    

    -3- Effects: Equivalent to: return underlying_.parse(ctx); Parses the format specifiers as a range-format-spec and stores the parsed specifiers in *this.

    If key-formatter_.set_debug_format() is a valid expression, and there is no range-underlying-spec, then calls key-formatter_.set_debug_format().

    If value-formatter_.set_debug_format() is a valid expression, and there is no range-underlying-spec, then calls value-formatter_.set_debug_format().

    -?- Returns: An iterator past the end of the range-format-spec.

    template<class FormatContext>
      typename FormatContext::iterator
        format(maybe-const-map& r, FormatContext& ctx) const;
    

    -4- Effects: Equivalent to: return underlying_.format(r, ctx); Writes the following into ctx.out(), adjusted according to the range-format-spec:

    1. STATICALLY-WIDEN<charT>("{") unless the n option is specified,

    2. — for each element e of the range r:

      1. — the result of writing get<0>(e) via key-formatter_,

      2. STATICALLY-WIDEN<charT>(": "),

      3. — the result of writing get<1>(e) via value-formatter_,

      4. STATICALLY-WIDEN<charT>(", "), unless e is the last element of r, and

    3. STATICALLY-WIDEN<charT>("}") unless the n option is specified.

    -?- Returns: An iterator past the end of the output range.