fmtlib / fmt

A modern formatting library
https://fmt.dev
Other
20.85k stars 2.51k forks source link

Ambiguity when using `format_as` with something that looks like a range #4202

Closed mattgodbolt closed 1 month ago

mattgodbolt commented 1 month ago

If we use a format_as for a range-like object, we get a compile error _if we include <fmt/ranges.h> only. This didn't happen in fmt 9.x but does in 10.x and newer.

class cstring_view {
public:
  using value_type = char;
  using pointer = char *;
  using const_pointer = const char *;
  using reference = char &;
  using const_reference = const char &;
  using iterator = char *;
  using const_iterator = const char *;
  using size_type = size_t;
  using difference_type = ptrdiff_t;
// either comment out these things
  const_iterator begin() const;
  const_iterator end() const;
  const_iterator cbegin();
  const_iterator cend();
};
// or comment this out...
std::string_view format_as(cstring_view csv);

#include <fmt/format.h>
// ...or comment out the line below
#include <fmt/ranges.h>

auto test(cstring_view view) {
    return fmt::format("{} {}", view, view);
}

https://compiler-explorer.com/z/97s7hcK4j

The error seems to be:

error: ambiguous template instantiation for 'struct fmt::v10::formatter<cstring_view, char, void>'

though it's hard to tell :)

Is there a way to tell fmt to "shoo, this isn't a range you should use" ?

CC @jeremy-rifkin @apmorton

mattgodbolt commented 1 month ago

@vitaut suggests a workaround of:

template <>
struct fmt::is_range<cstring_view, char> : std::false_type {};
vitaut commented 1 month ago

Closing as the ambiguity is legit: there are two formatters, one for a range and one defined via format_as, and there is already a trait to opt out of the range formatting.

aharrison24 commented 1 week ago

The big advantage of format_as is that we can use it in library code to make our types "{fmt} friendly", without needing to add a dependency on {fmt}. As soon as we need to start providing specializations of types in the fmt namespace then we have to include {fmt} headers.

Given that including fmt/ranges.h can silently change the way that other code formats things, if I include fmt/ranges.h in my library header in order to specialize fmt::is_range (even if wrapped in a __has_include check), then someone depending on my library might silently get different formatting behaviour for their other types.

I understand that there is an ambiguity if there are two different formatters available for a type. But there is an inconsistency in the rules, isn't there?

If someone has gone to the effort of adding a format_as overload for a type, shouldn't it take precedence over an implicit range formatter?

aharrison24 commented 1 week ago

Given that including fmt/ranges.h can silently change the way that other code formats things...

To be clear, I don't believe that's possible any more, but it was possible with earlier versions of {fmt} for types that were ranges and also had a stream insertion operator. My point is that I don't know what version of {fmt} consumers of my library will be using, so I want to minimize the possibility that I might change the behaviour of their code unexpectedly. That's why providing a simple format_as overload is so appealing.

vitaut commented 1 week ago

format_as is just a way to define a formatter (with a potential optimization at the call site) so it should be consistent. In both cases you have to opt out of range formatting if you want to customize formatting of a range.