Search code examples
c++stdfmt

C++ FMT issue formatting a custom abstract class


I'm working on an events system for a personal project and I'm trying to make the events be logged to a console as easy as LOG(event).

In this case, events are defined by an Event class which has some methods and a virtual ToString() function that returns a string with the event info and whatever I like to output on each event. This class is expanded further by defining specific event classes that inherit from Event class and that define the ToString() function according to what each event does and what variables it has.

So my LOG macro calls a static Log class' function to display the message in a console by converting the arguments into a string, like this:

#define LOG(...) Log::DisplayLog(LogUtils::StringFromArgs(__VA_ARGS__))

It's done like this because DisplayLog() receives also other info parameters that are not important for my problem.

The LogUtils::StringFromArgs() function converts the arguments to a string using fmt by doing the next:

template<typename FormatString, typename... Args>
inline std::string StringFromArgs(const FormatString& fmt, const Args &... args)
{
    char arg_string[1024];
    memset(arg_string, 0, sizeof(arg_string));
    fmt::format_to(arg_string, fmt, args...);
    return std::string(arg_string);
}

So, as I use fmt for this, I thought that making the log of an event as I want it would be easy, following the fmt guidelines, the idea was to set a formatter for the event class:

template<typename T>
struct fmt::formatter<T, std::enable_if_t<std::is_base_of<Event, T>::value, char>> : fmt::formatter<std::string>
{
    template<typename ParseContext>
    constexpr auto parse(ParseContext& ctx) { return ctx.begin(); }

    template<typename FormatCtx>
    auto format(const T& event, FormatCtx& ctx) // also tried Event& instead of T&
    {
        return fmt::format_to(ctx.out(), "{0}", event.ToString());
        // also tried:
        //return fmt::formatter<std::string>::format(event.ToString(), ctx);
    }

};

However, this is not working, when I try to do LOG(event) of a specific event class (let's say "event" is WindowResizedEvent inheriting from Event class) I keep having the same error from fmt because it can't format the event (also I tried to add a const char*() operator in the Event class and still the same):

enter image description here The "Argument 2" is the "event" argument (as I said I have some other arguments in the middle that aren't important on this problem).

Does anyone knows how can I get to format this without specifying a formatter for each type of event class? Because it would be always the same code for each event type.


Solution

  • The format string should be passed as fmt::format_string and not as a template parameter. Here's a working example (https://godbolt.org/z/xYehaMWsG):

    #include <fmt/format.h>
    
    struct Event {
      virtual std::string ToString() const = 0;
    };
    
    struct MyEvent : Event {
      std::string ToString() const override {
        return "foo";
      }
    };
    
    template<typename T>
    struct fmt::formatter<
        T, std::enable_if_t<std::is_base_of<Event, T>::value, char>>
        : fmt::formatter<std::string> {
      auto format(const T& event, fmt::format_context& ctx) {
        return fmt::format_to(ctx.out(), "{}", event.ToString());
      }
    };
    
    #define LOG(...) fmt::print("{}", StringFromArgs(__VA_ARGS__))
    
    template <typename... T>
    std::string StringFromArgs(fmt::format_string<T...> fmt, T&&... args) {
      return fmt::format(fmt, std::forward<T>(args)...);
    }
    
    int main() {
      LOG("{}", MyEvent());
    }
    

    Note that it's better to use fmt::format instead of fmt::format_to if you are formatting to a string. In fact you can replace StringFromArgs with fmt::format.