Search code examples
c++macrosinitializer-listvariadic

Passing brace enclosed initializer list to variadic macro and expanding to std::pair<>


I am currently working on a threaded Logging library as the first part of a bigger project for graphics drawing (for personal learning and development of skills).

currently i am using a singleton with a thread running on the side and taking Log messages and data into queues (so that they are not blocking events) to be processed later. I wrote a small wrapper around std::map<> as LogData that can be displayed by the logger in a stream or file in the following way.

[0.000155][DEBUG]: FILE LOGGER ADDED { ID="1" LVL="TRACE" }

The constructors allows to pass string,char*,floats,int,short long etc and will convert it to a string to be displayed later in thoses brackets.

Currently constructing this LogData is a bit bloaty. Example that produced the log above:

GG::LogData id_dat;
id_dat.push("ID", id);
id_dat.push("LVL",GG::loglevel_toString(lvl));
GG_DEBUG("FILE LOGGER ADDED", id_dat);

Since my Class is a Singleton i use macro's to allow for ease of use they are all the same as :

#define GG_TRACE(MESS, ...) GG::Logging::get()->push_to_queue(GG::LOG_LEVEL::TRACE, MESS, ##__VA_ARGS__);

This works fine for most use. But i wanted to make it possible to use on one line, and make it less bloaty. the effect i wanted to achieve was something like this :

//Desired Usage
GG_TRACE("VARIADIC TEST", {"X","1"}, {"Y","2"}, {"Z","3"});

this is expanded here:

void Logging::push_to_queue(GG::LOG_LEVEL level, std::string mess, std::pair<const char*, std::string> log_data ...)

I would use brace initialized list to produce the log data, then i would loop over the variadic argument and construct the LogData in a function instead of having to do it manually every time.

i had it working directly with a function like so.

void test(std::pair<char*,int> p) {
    GG::LogData dat;
    dat.push("key", p.first);
    dat.push("value", p.second);
    GG_TRACE("PAIR: ", dat);
}
// In main...
test({ "test",1 });

and that worked fine. But when i try to use that same pattern and have the macro's forward it to the push_to_queue fonction i get the following error with GCC. GCC compile error

Anybody have ever used brace enclosed initializer list in this way or know how to fix this bug ? i am fairly new to this kind of pattern. Any other suggestion or pointers to improve on this is appreciated. (sorry for the long post)


Solution

  • Note the declaration

    void Logging::push_to_queue(GG::LOG_LEVEL level, std::string mess,
        std::pair<const char*, std::string> log_data ...);
    

    does not have a variable number of parameters with type std::pair<const char*, std::string>. It's actually equivalent to the version with an added comma:

    void Logging::push_to_queue(GG::LOG_LEVEL level, std::string mess,
        std::pair<const char*, std::string> log_data,
        ...);
    

    which has one parameter of type std::pair<const char*, std::string>, and is a C-style variadic function, with arguments after the third only available via the <cstdarg> macros va_start, va_arg, and va_end. This can't be what you want, since there's no good way for such a function to know how many arguments there were, and since a braced list can never be an argument matching the C-style ellipsis.

    The only way to get a C++-style variadic function which knows the number (and types) of arguments is as a template with a variadic template parameter. But a braced list as argument means no template argument deduction, so it would be tricky to make this nice to use.

    But we can get this syntax working using a std::initializer_list, plus adding more {} in the macro:

    #include <initializer_list>
    #include <utility>
    #include <string>
    
    void GG::Logging::push_to_queue(GG::LOG_LEVEL level, std::string mess,
        std::initializer_list<std::pair<const char*, std::string>> log_data)
    {
        GG::LogData dat;
        for (const auto &kv : log_data)
            dat.push(kv.first, kv.second);
        // Do the rest...
    }
    
    #define GG_TRACE(MESS, ...) (GG::Logging::get()->push_to_queue( \
         GG::LOG_LEVEL::TRACE, MESS, {__VA_ARGS__}))
    

    So the expansion will have an argument like {{"X","1"}, {"Y","2"}, {"Z","3"}}, where the outer {} are for the std::initializer_list and the inner {} for each std::pair.