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

时间:2019-08-20 20:05:59

标签: c++ if-statement macros

我是一个设计cubesat(纳米卫星)的大学团队的成员。 同一子系统上的另一个人的任务是实现一个日志库,我们可以将其与错误流一起使用。 核心更改分别发生在两个文件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只是一个辅助函数constexpr,将每个级别与“全局”级别进行比较:

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. 不应该信任预处理器

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

最后,我觉得我没有尽可能地回答(和/或称呼)这个问题。 因此,随时对其进行修改:)

2 个答案:

答案 0 :(得分:1)

这里的一个问题是宏参数被使用了两次。如果在etl参数中调用了某些函数或使用了其他具有副作用的表达式,则该表达式(不必是常量表达式)可以被评估两次。也许没什么大不了的,因为在这种情况下,除了在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语法至少意味着C ++ 17,因此我假设了其他C ++ 17功能。首先,我们需要使if (declaration; expression)枚举数成为具有不同类型的对象,以便使用它们的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

答案 1 :(得分:0)

根据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)) \
      ...
  

为什么有人会选择实施这样的设计?

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

  

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

我认为此设计没有特定的陷阱。理解起来很复杂。这是您无法将宏替换为其他内容的一种情况,例如模板功能。