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.
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)
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
.