Search code examples
c++c++20fmt

fmt::formatter specialization not understood


I am using an opt-in method for enabling printing to the console via std::cout and fmt::print of custom classes. To do so I create a std::string to_string(const T& value) function that is left undefined in the general case. Specializing classes are supposed to:

  1. specialize to_string(const MyType& t)
  2. specialize struct printable< MyType >: public std::true_type{}

This will in turn activate std::ostream and fmt::formatter which specialize automatically for every printable type. A complete example is this:

#include <fmt/format.h>
#include <fmt/ostream.h>
#include <fmt/ranges.h>

#include <concepts>
#include <iostream>
#include <string>
#include <vector>

namespace common {

template <typename T>
std::string to_string(const T& value);

template <typename T>
struct printable : std::false_type {};

template <typename T>
constexpr bool printable_v = printable<T>::value;

}  // namespace common

template <typename T>
    requires(common::printable_v<T>)
auto& operator<<(std::ostream& os, const T& value) {
    return os << common::to_string(value);
}

template <class T>
    requires(common::printable_v<T>)
struct fmt::formatter<T> : public fmt::ostream_formatter {};

struct MyClass {
    std::vector<int> v{1, 2, 3, 4, 5};

    auto begin() { return v.begin(); }
    auto begin() const { return v.begin(); }
    auto end() { return v.end(); }
    auto end() const { return v.end(); }
};

namespace common {
inline std::string to_string(const MyClass& c) {
    return fmt::format("{}", c.v);
}

template <>
struct printable<MyClass> : std::true_type {};
}  // namespace common

int main() {
    // this works:
    // std::cout << common::to_string(MyClass{});
    // this doesn't:
    fmt::format("{}", MyClass{});
}

However, for fmt v10.1.1 this brings up somewhat cryptic error messages (clang-17):

/opt/compiler-explorer/gcc-13.2.0/lib/gcc/x86_64-linux-gnu/13.2.0/../../../../include/c++/13.2.0/type_traits:1048:21: error: static assertion failed due to requirement 'std::__is_complete_or_unbounded(std::__type_identity<fmt::formatter<MyClass, char, void>>{})': template argument must be a complete class or an unbounded array
 1048 |       static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}),

and

<source>:55:17: error: call to consteval function 'fmt::basic_format_string<char, MyClass>::basic_format_string<char[3], 0>' is not a constant expression
   55 |     fmt::format("{}", MyClass{});

The example can be found here on godbolt.

Why isn't my formatter method accepted and how can I make fmt understand it?


Solution

  • MyClass is a range (it has begin() and end() which return iterators). fmt has a default formatter for all ranges, which conflicts with the formatter you're trying to add. Neither is more specialized than the other, so it's ambiguous.

    If you disable the range formatter, then only yours will be used:

    template <typename Char>
    struct fmt::range_format_kind<MyClass, Char>
        : std::integral_constant<fmt::range_format, fmt::range_format::disabled>
    { };
    

    Once you do that, though, your actual customization for to_string isn't quite right (you'll get an undefined reference to common::to_string<MyClass> since your function won't be considered), but that's a separate problem.