I’ve recently started to write my own logging library which I encourage you to
check out. It’s called libsl. I’ve
decided to use fmt::format
for log message formatting and expose the most
basic interface for logging messages:
1
2
3
|
void log(const Logger::Level& level,
const std::string& msg,
std::source_location sl = std::source_location::current());
|
The premise here is that the user is responsible for providing a pre-formatted
message. Additionally, the log
function call collects the source location of
where it was called so, this information can be used in the log message as
well. You’d use the library the following way:
1
2
3
|
log(Logger::Level::Info, "this is a simple message");
...
log(Logger::Level::Error, fmt::format("a = {}, b = {}, c = {}", a, b, c));
|
This is all cool but I decided to improve the interface a bit to be able to do
something like so:
1
|
logfmt(Logger::Level::Info, "a = {}, b = {}, c = {}", a, b, c);
|
As a result, the fmt::format
will be called internally. This is where the
problems start and the story begins.
Variadic templates
One way to declare such interface would be through a variadic function template:
1
2
3
4
|
template <typename ... ArgsT>
void logfmt(const Logger::Level& level, std::string fmtStr, ArgsT&&... args) {
...
}
|
What about the std::source_location
? It can’t precede the argument pack,
because then, the automatic template deduction will fail. Consider the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class Logger {
public:
enum class Level {
Info,
Error
};
template <typename ... ArgsT>
void log(const Logger::Level& l,
std::string fmtStr,
std::source_location sl = std::source_location::current(),
ArgsT&& ... args) {
}
};
int main (int argc, char *argv[])
{
Logger logger;
int a = 123;
float b = 3.14;
std::string c = "hello";
logger.log(Logger::Level::Info, "a = {}, b = {}, c = {}", a, b, c);
return 0;
}
|
This won’t build (as expected):
1
2
3
4
5
6
7
8
9
10
|
variadic.cpp: In function ‘int main(int, char**)’:
variadic.cpp:27:15: error: no matching function for call to ‘Logger::log(Logger::Level, const char [23], int&, float&, std::string&)’
27 | logger.log(Logger::Level::Info, "a = {}, b = {}, c = {}", a, b, c);
| ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
variadic.cpp:13:10: note: candidate: ‘template<class ... ArgsT> void Logger::log(const Level&, std::string, std::source_location, ArgsT&& ...)’
13 | void log(const Logger::Level& l,
| ^~~
variadic.cpp:13:10: note: template argument deduction/substitution failed:
variadic.cpp:27:63: note: cannot convert ‘a’ (type ‘int’) to type ‘std::source_location’
27 | logger.log(Logger::Level::Info, "a = {}, b = {}, c = {}", a, b, c);
|
The opposite is not possible either:
1
2
3
4
5
6
7
|
template <typename ... ArgsT>
void log(
std::source_location sl,
const Logger::Level& l,
std::string fmtStr,
ArgsT&& ... args) {
}
|
The caller would have to provide the source_location
manually every time, which kind of defeats the point of having such API in the first place.
Solutions?
There are a couple of solutions, each with its own set of pros and cons.
Preprocessor
The most obvious one is a macro.
1
2
3
4
5
6
7
8
9
10
11
|
template <typename ... ArgsT>
void logApi(const Logger::Level& l,
std::string fmtStr,
std::source_location sl,
ArgsT&& ... args) {
}
#define log(level, fmtStr, ...) \
logApi(level, fmtStr, std::source_location::current(), __VA_ARGS__)
logger.log(Logger::Level::Info, "a = {}, b = {}, c = {}", a, b, c);
|
Although this works, this feels very wrong. Therefore I’m not gone deem this
approach as acceptable.
Custom deduction guides
The function could be converted to a function object - that would allow to
resolve automatic template arguments deduction problem by specifying a set of
deduction guides:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
class Logger {
public:
enum class Level {
Info,
Error
};
template <typename ... ArgsT>
void logApi(const Logger::Level& l,
std::string fmtStr,
std::source_location sl,
ArgsT&& ... args) {
}
};
template <typename ... ArgsT>
struct log {
log(Logger& l,
const Logger::Level& level,
std::string fmtStr,
ArgsT... args,
std::source_location sl = std::source_location::current()) {
l.logApi(level, fmtStr, sl, std::forward<ArgsT>(args)...);
}
};
template <typename ... ArgsT>
log(Logger&, const Logger::Level&, const char*, ArgsT...) -> log<ArgsT...>;
Logger logger;
int a = 123;
float b = 3.14;
std::string c = "hello";
log(logger, Logger::Level::Info, "a = {}, b = {}, c = {}", a, b, c);
|
That again, works but requires an intermediate object to be used by the client
which feels… weird.
Implicit type conversion
std::source_location
could be captured as a “side effect” of implicit
conversion. This solution is still a bit hacky but much better than any of the
prior ones. To do that, a wrapper object is needed:
1
2
3
4
5
6
7
8
9
|
struct SlWrapper {
Logger::Level level;
std::source_location sl;
SlWrapper(Logger::Level level,
std::source_location sl = std::source_location::current())
{
}
};
|
I’m exploiting an implicit conversion rules (which allow to convert one of
the constructor arguments) to convert Logger::Level
to SlWrapper
. At the same time,
the std::source_location
is captured. The compiler is allowed to construct
SlWrapper
solely from Logger::Level
:
1
2
|
SlWrapper w = Logger::Level::Info;
// SlWrapper contains source location as well now
|
Thanks to implicit conversion, the logfmt
can be implemented the following way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
struct SlWrapper {
Logger::Level level;
std::source_location sl;
SlWrapper(Logger::Level level, std::source_location sl = std::source_location::current()) :
level{level},
sl{sl}
{
}
};
void log(const Logger::Level& level,
std::string msg,
std::source_location sl = std::source_location::current()) {
// ...
}
template <typename ... ArgsT>
void logfmt(SlWrapper wrapper,
std::string fmtStr,
ArgsT&& ... args) {
log(wrapper.level,
fmt::format(fmt::runtime(fmtStr), std::forward<ArgsT>(args)...),
wrapper.sl);
}
int main (int argc, char *argv[])
{
Logger logger;
int a = 123;
float b = 3.14;
std::string c = "hello";
logger.logfmt(Logger::Level::Info, "a = {}, b = {}, c = {}", a, b, c);
return 0;
}
|
This is a perfect compromise between complexity and usability allowing
to achieve the desired API. Have a look on the implementation within
my logging library for more
details.