宏的 if 语句中的变量初始化
variable initialization inside macro's if statement
我是设计立方体卫星(纳米卫星)的大学团队的成员。
同一子系统中的另一个人的任务是实现一个日志库,我们可以将其与错误流一起使用。
核心变化发生在两个文件中,分别是 Logger.hpp
和 Logger.cpp
。
他#define
不同"log levels",每个级别对应一个错误的严重程度:
#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
[...]
};
另外,他引入了"global level"的概念。
也就是说,只有严重程度与全局级别相同或更高级别的错误才会被记录。
要设置"global level",您需要设置上述常量之一,例如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
只是一个辅助 constexpr
ed 函数,它将每个级别与 "global" 级别进行比较:
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
是我的朋友,
- 宏可能很危险
- 不应信任预处理器
这就是为什么围绕宏构建的设计让我感到害怕,但我不知道这种担忧是否有效,或者是否源于我缺乏理解。
最后,我觉得我没有把问题的短语(and/or 标题)做到尽可能好。
所以请随意修改它:)
根据 description of if statement in cppreference.com,如果您在 if 条件中使用初始化语句,如下所示:
if constexpr(optional) ( init-statement(optional) condition )
statement-true
else
statement-false
那么这将等同于:
{
init_statement
if constexpr(optional) ( condition )
statement-true
else
statement-false
}
因此,这意味着在您的情况下,entry
变量将在整个 if 语句的范围完成后立即超出范围。此时,入口对象的析构函数被调用,您将记录有关当前作用域指令的一些信息。此外,要使用 if constexpr
语句,您应该像这样更新您的宏:
#define LOG(level)
if constexpr (Logger::isLogged(level)) \
...
Why would anyone choose to go with implementing a design as this?
因此,使用 if constexpr
语句允许您在编译时检查条件,如果条件为假,则不要编译 statement-true
。如果您在代码中大量使用日志记录语句,并且不想在不需要日志记录时让二进制文件变大,则可以继续使用这种方法。
What are the pitfalls to look out for with this approach, if any?
我看不出这种设计有什么特别的缺陷。只是理解起来很复杂。这是您不能用其他东西替换宏的情况之一,例如模板函数。
这里的一个问题是宏参数被使用了两次。如果在 LOG()
参数中调用了某个函数或使用了其他具有副作用的表达式,则该表达式(不需要是常量表达式)可能会被求值两次。也许没什么大不了的,因为在这种情况下,除了 LOG()
.
中的直接 LogLevel
枚举器外,没有什么理由使用其他任何东西
另一个危险的陷阱:考虑像
这样的代码
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 {}; }
我是设计立方体卫星(纳米卫星)的大学团队的成员。
同一子系统中的另一个人的任务是实现一个日志库,我们可以将其与错误流一起使用。
核心变化发生在两个文件中,分别是 Logger.hpp
和 Logger.cpp
。
他#define
不同"log levels",每个级别对应一个错误的严重程度:
#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
[...]
};
另外,他引入了"global level"的概念。
也就是说,只有严重程度与全局级别相同或更高级别的错误才会被记录。
要设置"global level",您需要设置上述常量之一,例如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
只是一个辅助 constexpr
ed 函数,它将每个级别与 "global" 级别进行比较:
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
是我的朋友,
- 宏可能很危险
- 不应信任预处理器
这就是为什么围绕宏构建的设计让我感到害怕,但我不知道这种担忧是否有效,或者是否源于我缺乏理解。
最后,我觉得我没有把问题的短语(and/or 标题)做到尽可能好。 所以请随意修改它:)
根据 description of if statement in cppreference.com,如果您在 if 条件中使用初始化语句,如下所示:
if constexpr(optional) ( init-statement(optional) condition )
statement-true
else
statement-false
那么这将等同于:
{
init_statement
if constexpr(optional) ( condition )
statement-true
else
statement-false
}
因此,这意味着在您的情况下,entry
变量将在整个 if 语句的范围完成后立即超出范围。此时,入口对象的析构函数被调用,您将记录有关当前作用域指令的一些信息。此外,要使用 if constexpr
语句,您应该像这样更新您的宏:
#define LOG(level)
if constexpr (Logger::isLogged(level)) \
...
Why would anyone choose to go with implementing a design as this?
因此,使用 if constexpr
语句允许您在编译时检查条件,如果条件为假,则不要编译 statement-true
。如果您在代码中大量使用日志记录语句,并且不想在不需要日志记录时让二进制文件变大,则可以继续使用这种方法。
What are the pitfalls to look out for with this approach, if any?
我看不出这种设计有什么特别的缺陷。只是理解起来很复杂。这是您不能用其他东西替换宏的情况之一,例如模板函数。
这里的一个问题是宏参数被使用了两次。如果在 LOG()
参数中调用了某个函数或使用了其他具有副作用的表达式,则该表达式(不需要是常量表达式)可能会被求值两次。也许没什么大不了的,因为在这种情况下,除了 LOG()
.
LogLevel
枚举器外,没有什么理由使用其他任何东西
另一个危险的陷阱:考虑像
这样的代码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 {}; }