How to perform different behaviors based on the length of variadic args in cpp macro?

125 views Asked by At

I'm trying to make a short-hand for logging (log4cplus) + libfmt. I have defined a macro in my util header:

#define T_TRACE(fs, ...) \
    LOG4CPLUS_TRACE(Logger::Instance().logger, fmt::format(fs, __VA_ARGS__).c_str())

Then, every other cpp just include this header, invoke T_TRACE to perform logging. e.g.
T_TRACE("Version is {}", "1.0.0");
Everything works fine until I want to log a string contains {} pair (a simple json string).
T_TRACE("{\"name\": \"value\"}"), the fmt lib will treat it as a formatter, so I need to inform the macro that insert a default placeholder "{}" for me if there is only one argument.

The reason for using macro is that I also want to log the source file and lines. If I wrap it in a function, all the file/line info would be in that wrapper function.

3

There are 3 answers

0
Eric Qiang On BEST ANSWER

Finally, I come up with a solution based on Kramer's answer.
Unamed logging: T_TRACE(config) => FMT_SERIALIZER_RESULT

#define T_TRACE(o, ...) \
    LOG4CPLUS_TRACE(Logger::Instance().logger, LogImpl(o, __VA_ARGS__).c_str())

template <typename T, typename... args_t>
inline auto LogImpl(const T& obj, args_t&&... args)
{
    if constexpr (sizeof...(args_t))
        return fmt::format(obj, args...);
    else
        return fmt::format("{}", obj);
}

Named logging: T_TRACE(config) => config: FMT_SERIALIZER_RESULT

#define T_TRACE(o, ...) \
    LOG4CPLUS_TRACE(Logger::Instance().logger, LogImpl(#o, o, __VA_ARGS__).c_str())

template <typename T, typename... args_t>
inline auto LogImpl(const char* name, const T& obj, args_t&&... args)
{
    if constexpr (sizeof...(args_t))
        return fmt::format(obj, args...);
    else
        return fmt::format("{}: {}", name, obj);
}
2
Pepijn Kramer On

The real solution is to NOT using macros at all (you should use them only if there is no other option anyway). In most cases you can use variadic function templates. Also note that if you provide a runtime format string you need to use fmt::vformat and fmt::make_format_args.

template<typename... args_t>
void T_TRACE(std::string_view format_string, args_t&&...args)
{
    auto output = fmt::vformat(format_string, fmt::make_format_args<args_t>(args));
    LOG4CPLUS_TRACE(Logger::Instance().logger, output.c_str());
}

If you would still need to change behavior based on the number of arguments (which I doubt) you can use sizeof...(args_t) and if constexpr(sizeof...(args_t)) == some_constant_value)

0
ecatmur On

This is a job for Boost.Preprocessor.

#define T_TRACE(...) \
    LOG4CPLUS_TRACE(Logger::Instance().logger, fmt::format( \
        BOOST_PP_IF(BOOST_PP_GREATER(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 1), \
        BOOST_PP_EMPTY, "{}" BOOST_PP_COMMA)() __VA_ARGS__).c_str())

Example. Note that fs is not taken as a separate parameter, since before C++20 empty variadic macro arguments are very tricky to process portably.

It would be a lot simpler if you were using C++20 or later, since that has __VA_OPT__ and thus BOOST_PREPROCESSOR_VA_OPT:

#define T_TRACE(fs, ...) \
    LOG4CPLUS_TRACE(Logger::Instance().logger, fmt::format( \
        BOOST_PP_VA_OPT((fs,), ("{}", fs), __VA_ARGS__) __VA_ARGS__).c_str())

However, since C++20 you can just use std::source_location, which makes this macro (though not some other macros) unnecessary.