0

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.

LuchoSuaya
  • 77
  • 10
  • 1
    `LOG(event)`: Your macro/function expects a format string as first argument. – user17732522 Dec 30 '21 at 20:40
  • No, the problem is on the fmt::format_to() function called from StringFromArgs(), not in the macro, the macro doesn't expects a type of variable – LuchoSuaya Dec 30 '21 at 21:08
  • 1
    The second argument to `fmt::format_to` must be convertible to a format string. The way you presented your code, `LOG` just passes on its first argument to the second one of `format_to`. `Event` does not look like a valid format string or convertible to one. – user17732522 Dec 30 '21 at 21:10
  • Ah yes, but the point is how to do that (as answered below) – LuchoSuaya Jan 02 '22 at 16:03
  • The whole point of `fmt` is that you can write a format string like e.g. `"Error at line {} in object {}."` and have the `{}` be replaced with a string encoding some object passed to the `format_to` function. If you don't need such a format string, then you simply want a conversion operator to `std::string`. – user17732522 Jan 02 '22 at 16:10
  • Yes, that was my thought, simply doing an operator in the `Event` class converting an event to a const char* or a string but fmt still gives the same error. – LuchoSuaya Jan 02 '22 at 16:12
  • No, that is not what I mean. What I mean is that you don't need any `fmt` at all if you don't want to use format strings. Just have `StringFromArgs` concatenate all the arguments after conversion to `std::string`. – user17732522 Jan 02 '22 at 16:13
  • Maybe you should give some explicit examples of what you expect the `LOG` macro to produce for single and multiple arguments. I am not entirely clear on what you expect. – user17732522 Jan 02 '22 at 16:15
  • Aaah yes, I see what you mean, I can give it a try. Yeah so the point is to use it as `LOG(event)` if I don't want to specify any further strings and as a `LOG("Whatever Message {}", value)` for other cases, that's why I'm dealing with fmt, but I think what you say makes pretty much sense – LuchoSuaya Jan 02 '22 at 16:25
  • In that case I would add another overload for that case and let it either convert to `std::string` or call `format_to` with `"{}"` format string. Making the overload resolution work correctly might be a bit tricky. Probably needs SFINAE or a `requires` clause to exclude arguments convertible to format strings. I haven't considered the details. – user17732522 Jan 02 '22 at 16:31
  • Okay thanks, that actually worked perfectly :) ... I didn't needed any SFINAE for the Event-inheriting classes (at least for now, I'll see in the future if other types give issues), so what I did is, as you said, overload the function and have one that gets one arg T& and another one that gets the format_string and the args, and with that I could make it work... If you want to make your answer actually an answer down the page I'll be happy to mark it as an answer :) (I don't know if I can mark 2 answers because the @vitaut one was helpful too, I'll see if it's an option). – LuchoSuaya Jan 02 '22 at 16:51
  • Is a bit crazy because the overloading is an easy & simple solution that I didn't thought of before :D – LuchoSuaya Jan 02 '22 at 16:52

1 Answers1

2

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.

vitaut
  • 49,672
  • 25
  • 199
  • 336
  • Thanks! That worked :) ... Additionally, is there a way to avoid the "{}" parameter? Since it expects a fmt::format_string I understand that it needs that string, but could I arrange it in a way in which I could just do `LOG(event)`? Is there any fmt type that would allow me to do that? – LuchoSuaya Jan 01 '22 at 22:50
  • Do you mean overload LOG so that it takes both format string and a single argument without format string? If yes then I wouldn't recommend it because of ambiguity. – vitaut Jan 02 '22 at 01:49
  • No, I wasn't unaware that there was a way to overload macros... I wanted to know if fmt supports passing some general type instead of a format string, I know it doesn't makes a lot of sense or that is very generic but I have very little experience with fmt – LuchoSuaya Jan 02 '22 at 15:48
  • Maybe it's worth trying by making the DisplayLog() function to work as a template or actually overloading that function – LuchoSuaya Jan 02 '22 at 15:50