0

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?

10
  • 3
    You either have to do the dispatch automatically by virtual methods, or by a "visitor", but then, you have to know the set of types to consider... Commented May 20 at 8:42
  • Okay, so I guess the answer effectively is "it doesn't work, unless you are facing closed-set polymorphism"? Commented May 20 at 8:51
  • But format already has polymorphism, it is just static. You don't have to provide a virtual function you can just check if the class implements say a non-virtual "std::string description()" method (at compile time) and call that if is has it. (Do not implement that function in the baseclass) Commented May 20 at 9:14
  • @PepijnKramer If I understood you correctly, I think that wouldn't work for an object of type Fruit & as that would require a specialization for std::formatter<Fruit> and as you suggested, Fruit::description() doesn't exist. Commented May 20 at 9:19
  • What would your desired output be? Because I think this is not quite it : godbolt.org/z/s6sEcx1TM Commented May 20 at 9:33

3 Answers 3

2

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.

The precise phrase for "exact type" is dynamic type. Very little depends on the dynamic type, ignoring the type. So this is the most constrained part of the problem.

For example, typeid is one of these things:

auto format(const Fruit& obj, std::format_context& ctx) const {
    const auto descriptions = std::map<std::type_index, std::string>{
        {typeid(Fruit), "Fruit"},
        {typeid(Apple), "Apple"},
        {typeid(Banana), "Banana"},
    };
    if (auto it = descriptions.find(typeid((obj)));
        it != descriptions.end()) {
        return std::formatter<std::string_view>::format(it->second, ctx);
    } else {
        return std::formatter<std::string_view>::format(
            "Some kind of Fruit", ctx);
    }
}

See in practice here: https://godbolt.org/z/111qKnrac

More obviously, virtual functions are another. However, that is intrusive as noted.

Sign up to request clarification or add additional context in comments.

Comments

2

Your goal is to change behavior based on the dynamic type of your Fruit, without doing it intrusively.

Your goal is in conflict with other goals of dynamic typing.

You can examine the exact type of your Fruit by using RTTI (run time type information), and either checking the dynamic typeid of the object, or using dynamic_cast to linearly check which subtype it is.

Once you have done this, however, you can no longer create pseudo-Oranges. A pseudo-Orange is a class that pretends to be an orange, but has some quirk that is not exposed in the interface. For example, a pseudo-Orange might add logging functionality to Oranges, or enforce a requirement that no more than 10 Oranges exist, or something else.

If everyone stays within the constraints of the interface of Fruit, then a pseudo-Orange can drop-in replace every (or a select subset) of Oranges, and the program cannot tell it was replaced.

Once you add in intrusive operations like dynamic_cast and typeid to your code this entire "substiution by equivalent behavior" no longer works; the exact C++ type of every object becomes part of the interface of your Fruit, which is a huge increase in your API surface.

In formal OO, Fruit is supposed to expose everything that the owner of an object with interface Fruit is supposed to know about the object in question. If you need to downcast to get extra information, your Fruit interface has failed.

Wanting to know what kind of Fruit the interface is is like wanting to know if the int you are printing is on the stack or the heap. You can hack the C++ language and implementation to figure this out on most platforms (create an object at the start of main, store its address as an integer, do the same in your "test if an object is on the stack" code, check if the address if your object is between those two integers), but doing so is a very intrusive thing that probably shouldn't be done, and if your business logic depends on it, maybe rethink your business logic.

In short, a reasonable API for struct Fruit {}; would probably include a virtual std::string FruitType() const = 0; anyhow (or something similar), and formatting a Fruit should consist of calling that function instead of downcasting, if being able to categorize fruit was part of the Fruit API. If categorization of fruit is not part of the API, any attempt to do so while printing is a violation of the Fruit API.

Comments

1

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

1 Comment

I'm marking this as the accepted answer as this is the most complete answer to my question as posed. More generally though, the answer for myself is that std::format is not meant to replace virtual member functions when it comes to formatting of polymorphic objects. Thanks for the complete example though!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.