c++formatpolymorphismc++20

Polymorphic std::format that allows overriding


In order to use C++20's format capabilities with custom types, we have to provide template specializations of std::formatter for every type we want to format. The nice thing about this approach is that formatting support for any given class can be added in a non-intrusive way.

However, what if I want to format a polymorphic type? So as a quick example, let's consider

struct Fruit {};
struct Apple : Fruit {};
struct Banana : Fruit {};

So assume that I have an object of type Fruit & and want to format it, but I want the formatted output to depend on the exact type of the object. That is, I want Apple and Banana to be formatted differently. Adding specializations std::formatter<Apple> and std::formatter<Banana> helps only if we have objects of type Apple or Banana but they won't be used for a Fruit &. Of course, we could specialize std::formatter<Fruit> but this would then always be used for Fruit & regardless of whether the underlying object is an Apple or a Banana.

We could also do something like (Godbolt)

template <typename F>
requires std::is_base_of_v<Fruit, F>
struct std::formatter<F> {
    constexpr auto parse(std::format_parse_context& ctx) {
        return ctx.begin();
    }

    auto format(const F& obj, std::format_context& ctx) const {
        return std::format_to(ctx.out(), "{}", "Some kind of Fruit");
    }
};

but in order to achieve the desired output, Fruit would need a virtual function that is being called to do the formatting which can be called in this std::formatter specialization. However, this would require intrusive changes to the classes.

Hence, my question is: is there any way to obtain a "virtual template specialization" such that I can implement formatting support for polymorphic objects in a non-intrusive way that allows for sub-classes to overwrite the formatting of their parent classes?


Solution

  • If you are willing to forego a bit of type safety and use typeid, you can keep a registry of formatting functions:

    // Taken from the std::type_info::hash_code example at cppreference.com
    using TypeInfoRef = std::reference_wrapper<const std::type_info>;
     
    struct Hasher {
        std::size_t operator()(TypeInfoRef code) const
        {
            return code.get().hash_code();
        }
    };
     
    struct EqualTo {
        bool operator()(TypeInfoRef lhs, TypeInfoRef rhs) const
        {
            return lhs.get() == rhs.get();
        }
    };
    
    using FruitFormatter = std::function<std::format_context::iterator(const Fruit&, std::format_context&)>;
    
    // TODO: make sure Fruit is a base of T
    template <class T>
    typename FruitFormatter::result_type fruitFormatter(const Fruit& f, std::format_context& ctx) {
        return std::formatter<T>{}.format(dynamic_cast<const T&>(f), ctx);
    }
    
    // For exposition purposes, this is a static enumeration. You can add members explicitly in code or by using static initialization.
    std::unordered_map<TypeInfoRef, FruitFormatter, Hasher, EqualTo> fruit_formatters = {
        {typeid(Apple), fruitFormatter<Apple>},
        {typeid(Banana), fruitFormatter<Banana>},
    };
    
    template<>
    struct std::formatter<Fruit, char> {
        constexpr auto parse(std::format_parse_context& ctx) {
            return ctx.begin();
        }
    
        auto format(const Fruit& obj, std::format_context& ctx) const {
            auto& ti = typeid(obj);
            if (ti == typeid(Fruit)) {
                return std::format_to(ctx.out(), "{}", "Some kind of Fruit");
            } else {
                return fruit_formatters[ti](obj, ctx);
            }
        }
    };
    

    You can then write

    int main()
    {
        std::println("{}", Apple{});
        std::println("{}", Banana{});
    
        Apple a;
        Fruit &f = a;
        std::println("{}", f);
    }
    

    and have it print

    Apple
    Banana
    Apple