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?
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