I'm a member of a university team designing a cubesat (nanosatellite). Another guy on the same subsystem was tasked to implement a logging library that we can use with the error stream. The core changes happen in two files, Logger.hpp and Logger.cpp, respectively.
He #defines different "log levels", each level corresponding to the severity of an error:
#if defined LOGLEVEL_TRACE
#define LOGLEVEL Logger::trace
#elif defined LOGLEVEL_DEBUG
#define LOGLEVEL Logger::debug
#elif defined LOGLEVEL_INFO
[...]
#else
#define LOGLEVEL Logger::disabled
#endif
Levels are inside of an enum:
enum LogLevel {
trace = 32, // Very detailed information, useful for tracking the individual steps of an operation
debug = 64, // General debugging information
info = 96, // Noteworthy or periodical events
[...]
};
Additionally, he introduces the concept of "global level". That is, only errors with a level as severe as the global level's one or higher will be logged. To set the "global level", you need to set one of the constants mentioned above, such as LOGLEVEL_TRACE. More on that below.
Last but not least, he creates a custom stream and uses some macro magic to make logging easy, just by using the << operator:
template <class T>
Logger::LogEntry& operator<<(Logger::LogEntry& entry, const T value) {
etl::to_string(value, entry.message, entry.format, true);
return entry;
}
This question is about the following piece of code; he introduces a fancy macro:
#define LOG(level)
if (Logger::isLogged(level)) \
if (Logger::LogEntry entry(level); true) \
entry
isLogged is just a helper constexpred function that compares each level with the "global" one:
static constexpr bool isLogged(LogLevelType level) {
return static_cast<LogLevelType>(LOGLEVEL) <= level;
}
I have never seen using macros like this, and before I go on with my question, here's his explanation:
Implementation details
This macro uses a trick to pass an object where the << operator can be used, and which is logged when the statement
is complete.
It uses an if statement, initializing a variable within its condition. According to the C++98 standard (1998), Clause 3.3.2.4,
"Names declared in the [..] condition of the if statement are local to the if [...]
statement (including the controlled statement) [...]".
This results in the Logger::LogEntry::~LogEntry() to be called as soon as the statement is complete.
The bottom if statement serves this purpose, and is always evaluated to true to ensure execution.
Additionally, the top `if` checks the sufficiency of the log level.
It should be optimized away at compile-time on invisible log entries, meaning that there is no performance overhead for unused calls to LOG.
This macro seems cool, but makes me somewhat uneasy and my knowledge isn't sufficient to be able to form a proper opinion. So here goes: * Why would anyone choose to go with implementing a design as this? * What are the pitfalls to look out for with this approach, if any? * (bonus) If this approach isn't considered good practice, what could be done instead?
The full code section can be seen here
What surprised (and alerted) me the most is that while the idea behind this doesn't seem too complicated, I couldn't find a similar example anywhere on the internet. I've come to learn that constexpr is my friend and that a) macros can be dangerous b) the preprocessor shouldn't be trusted This is why a design built around a macro scares me, but I don't know whether this concern is valid, or whether it stems from my lack of understanding.
Lastly, I feel that I didn't phrase (and/or title) the question nearly as good as one could. So feel free to modify it :)
Aucun commentaire:
Enregistrer un commentaire