宏的 if 语句中的变量初始化

variable initialization inside macro's if statement

本文关键字:变量 初始化 if 语句 宏的      更新时间:2023-10-16

我是设计立方体卫星(纳米卫星(的大学团队的成员。 同一子系统上的另一个人的任务是实现一个我们可以与错误流一起使用的日志记录库。 核心更改分别发生在两个文件中,Logger.hppLogger.cpp

#define不同的"日志级别",每个级别对应于错误的严重性:

#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

关卡在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
[...]
};

此外,他还引入了"全球层面"的概念。 也就是说,只会记录级别与全局级别一样严重的错误。 要设置"全局级别",您需要设置上述常量之一,例如LOGLEVEL_TRACE。 更多内容见下文。

最后但并非最不重要的一点是,他创建了一个自定义流,并使用一些宏魔法使日志记录变得容易,只需使用<<运算符:

template <class T>
Logger::LogEntry& operator<<(Logger::LogEntry& entry, const T value) {
etl::to_string(value, entry.message, entry.format, true);
return entry;
}

这个问题是关于以下一段代码的;他引入了一个花哨的宏:

#define LOG(level)
if (Logger::isLogged(level)) 
if (Logger::LogEntry entry(level); true) 
entry

isLogged只是一个辅助constexpred 函数,它将每个级别与"全局"级别进行比较:

static constexpr bool isLogged(LogLevelType level) {
return static_cast<LogLevelType>(LOGLEVEL) <= level;
}

我从未见过使用这样的宏,在我继续我的问题之前,以下是他的解释:

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.

这个宏看起来很酷,但让我有些不安,我的知识不足以形成正确的意见。 所以这里是:

  • 为什么有人会选择像这样实现设计?
  • 这种方法需要注意哪些陷阱(如果有的话(?
  • (奖金(如果这种方法不被认为是好的做法,那么可以做些什么呢?

最让我惊讶(和提醒(的是,虽然这背后的想法似乎不太复杂,但我在互联网上的任何地方都找不到类似的例子。 我开始知道constexpr是我的朋友,

而且
  1. 宏可能很危险
  2. 不应信任预处理器

这就是为什么围绕宏构建的设计让我感到害怕,但我不知道这种担忧是否有效,或者它是否源于我的缺乏理解。

最后,我觉得我没有尽可能好地表达(和/或标题(这个问题。 因此,请随意修改它:)

这里的一个问题是宏参数使用了两次。如果在LOG()参数中调用了某个函数或使用了其他具有副作用的表达式,则该表达式(不必是常量表达式(可以计算两次。 也许没什么大不了的,因为在这种情况下,除了直接LogLevel枚举器之外,几乎没有理由在LOG()中使用任何其他东西。

另一个危险的陷阱:考虑像这样的代码

if (!test_valid(obj))
LOG(Logger::info) << "Unexpected invalid input: " << obj;
else
result = compute(obj);

展开宏将其变成

if (!test_valid(obj))
if (Logger::isLogged(Logger::info))
if (Logger::LogEntry entry(Logger::info); true)
entry << "Unexpected invalid input: " << obj;
else
result = compute(obj);

无论全局日志级别如何,都永远无法调用compute函数!

如果你的团队确实喜欢这种语法,下面是一种获得更安全行为的方法。if (declaration; expression)语法至少意味着 C++17,所以我假设其他 C++17 功能。 首先,我们需要LogLevel枚举器是不同类型的对象,以便使用它们的LOG表达式可以具有不同的行为。

namespace Logger {
template <unsigned int Value>
class pseudo_unscoped_enum
{
public:
constexpr operator unsigned int() const noexcept
{ return m_value; }
};
inline namespace LogLevel {
inline constexpr pseudo_unscoped_enum<32> trace;
inline constexpr pseudo_unscoped_enum<64> debug;
inline constexpr pseudo_unscoped_enum<96> info;
}
}

接下来,定义一个虚拟记录器对象,该对象支持operator<<但不执行任何操作。

namespace Logger {
struct dummy_logger {};
template <typename T>
dummy_logger& operator<<(dummy_logger& dummy, T&&)
{ return dummy; }
}

LOGLEVEL可以保留其相同的宏定义。最后,几个重载的函数模板替换了LOG宏(可能在全局命名空间中(:

#include <type_traits>
template <unsigned int Level,
std::enable_if_t<(Level >= LOGLEVEL), std::nullptr_t> = nullptr>
LogEntry LOG(pseudo_unscoped_enum<Level>) { return LogEntry(Level); }
template <unsigned int Level,
std::enable_if_t<(Level < LOGLEVEL), std::nullptr_t> = nullptr>
dummy_logger LOG(pseudo_unscoped_enum<Level>) { return {}; }

根据 cppreference.com 中 if 语句的描述,如果在 if 条件中使用 init 语句,如下所示:

if constexpr(optional) ( init-statement(optional) condition ) 
statement-true 
else 
statement-false

那么这将等效于:

{
init_statement 
if constexpr(optional) ( condition ) 
statement-true 
else
statement-false 
}

因此,这意味着在您的情况下,一旦完成整个 if 语句的范围,entry变量就会超出范围。此时,将调用条目对象的析构函数,您将记录有关当前范围指令的一些信息。此外,要使用if constexpr语句,应按如下所示更新宏:

#define LOG(level)
if constexpr (Logger::isLogged(level)) 
...

为什么有人会选择像这样实现设计?

因此,使用if constexpr语句允许您在编译时检查条件,如果条件为 false,则不要编译statement-true。如果您在代码中大量使用日志记录语句,并且不想在不需要日志记录时使二进制文件变大,则可以继续使用此方法。

这种方法需要注意哪些陷阱(如果有的话(?

我认为这种设计没有特定的陷阱。理解起来很复杂。这是您不能用其他东西替换宏的情况之一,例如模板函数。