在阅读chromium源码的时候,在\blink render core的base\check.h头文件中,发现了这个定义:
#define CHECK(condition, ...) \
LOGGING_CHECK_FUNCTION_IMPL( \
::logging::CheckError::Check(#condition __VA_OPT__(, ) __VA_ARGS__), \
condition)
#define PCHECK(condition) \
LOGGING_CHECK_FUNCTION_IMPL(::logging::CheckError::PCheck(#condition), \
condition)
其中 __VA_OPT__
引起了我的注意。
什么是__VA_OPT__
__VA_OPT__
被设计用于处理可变参数宏(variadic macros)中的逗号插入或移除问题。
这个宏是在C++20标准中引入的,用于解决在可变参数宏中逗号操作的不确定性,特别是在需要条件性地添加或去除逗号的情况下。
可变参数宏简介
在C++中,可变参数宏允许宏接受不定数量的参数。它们由 _VA_ARGS_
标识符标识,代表了宏调用中传递的所有额外参数。在C++11中,我们可以这样定义一个可变参数宏:
#define PRINT(...) printf(__VA_ARGS__)
__VA_OPT__
的作用
__VA_OPT__
的主要用途是在宏展开时控制逗号的出现。在宏中,逗号 ,
是一个特殊的字符,它用于参数列表中的参数分隔符。在某些情况下,我们需要在宏展开时有条件地插入或移除逗号。
例如,假设我们想创建一个宏,该宏可以在参数之间插入一个字符串,但在最后一个参数之后不应该有逗号:
#define JOIN(...) JOIN_IMPL(__VA_ARGS__ __VA_OPT__(,))
#define JOIN_IMPL(...) __VA_ARGS__" joined by "__VA_ARGS__
在这个例子中,__VA_OPT__(,)
将在有多个参数时插入一个逗号,但在只有一个参数时不会。如果宏调用是 JOIN(1, 2)
,则结果将是 "1 joined by 2"
;但如果调用是 JOIN(1)
,则结果将是 "1"
,没有多余的逗号。
扩展思考,其他相关宏
__VA_ARGS__
:这是可变参数宏的参数包,代表了传递给宏的任意数量的参数。__VA_LIST__
:在C++11中引入,用于在宏内部表示参数包。但在C++17之后,__VA_ARGS__
成为了首选,因为它的行为更明确。__VA_START__
和__VA_END__
:这两个宏在C++20中被提议,但最终没有被标准化。它们的目的是用于标记参数包的开始和结束,以便在宏体内安全地操作参数包。
使用 __VA_OPT__
的注意事项
__VA_OPT__
必须在宏的参数列表中紧随参数包之后。__VA_OPT__
后面的括号中必须包含一个逗号或空参数,否则它将不起作用。__VA_OPT__
的使用需要编译器支持。例如,在MSVC中,__VA_OPT__
的支持是需要通过编译选项/Zc:preprocessor
打开才能使用。因为笔者刚试了一下,发现不添加这个选项会报错,并提供warning指引:
- warning C5109: 在宏中使用 VA_OPT 需要“/Zc:preprocessor”
简而言之,__VA_OPT__
提供了一种简洁的方法来处理可变参数宏中的逗号逻辑,使得宏定义更加灵活和健壮。
理解开头的Chromium中的代码片段
开头的宏定义 CHECK
是一个常见的模式,用于在代码中插入条件检查,通常用于调试或断言条件。
现在,让我们逐步分解这个宏定义以更好地理解它:
#define CHECK(condition, ...) \
LOGGING_CHECK_FUNCTION_IMPL( \
::logging::CheckError::Check(#condition __VA_OPT__(, ) __VA_ARGS__), \
condition)
逐步分解:
-
LOGGING_CHECK_FUNCTION_IMPL
: 接受两个参数。第一个参数是::logging::CheckError::Check
函数的调用表达式,第二个参数是condition
。 -
::logging::CheckError::Check
: 这个函数用于执行实际的条件检查。它接收一个字符串(表示condition
的文本形式)和__VA_ARGS__
(额外的参数,通常用于日志记录)。 -
#condition
: 这个操作符将condition
表达式转换成一个字符串。在宏展开时,#condition
将变成一个字符串字面量,表示condition
的源代码文本。 -
__VA_OPT__(, )
: 这个表达式利用了我们之前讨论过的__VA_OPT__
宏。它会在__VA_ARGS__
存在且非空时插入一个逗号。这里的逗号是必需的,因为它用于分隔#condition
和__VA_ARGS__
。 -
__VA_ARGS__
: 这是一个可变参数包,可以接收零个或多个参数。这些参数通常用于日志记录,提供关于失败条件的更多信息。
CHECK宏的使用示例:
例如这样使用 CHECK
宏:
int x = 1;
int y = 2;
CHECK(x == y, "x should be equal to y, but x is %d and y is %d", x, y);
宏的展开:
宏 CHECK
展开后的代码大致如下:
LOGGING_CHECK_FUNCTION_IMPL(
::logging::CheckError::Check("x == y" __VA_OPT__(, ) "x should be equal to y, but x is %d and y is %d", x, y),
x == y);
由于 __VA_ARGS__
不为空,__VA_OPT__
会插入一个逗号,所以 __VA_OPT__(, )
展开后的代码变成,
:
LOGGING_CHECK_FUNCTION_IMPL(
::logging::CheckError::Check("x == y", "x should be equal to y, but x is %d and y is %d", x, y),
x == y);
这将调用 ::logging::CheckError::Check
函数,并传递两个参数:一个是 x == y
的字符串表示,另一个是错误消息和附加参数。同时,它还会检查 x == y
是否为真。如果条件不满足,通常会导致程序中断并输出错误信息。
这种模式在许多日志和调试框架中都很常见。
头脑风暴:其他使用场景
条件性插入文本:
你可以使用 VA_OPT 来基于 VA_ARGS 是否为空来决定是否插入某些文本。
#define LOG_PREFIX(prefix, ...) LOG_IMPL(prefix __VA_OPT__(:) __VA_ARGS__)
#define LOG_IMPL(prefix, ...) printf("%s %s\n", prefix, __VA_ARGS__)
LOG_PREFIX("INFO", "Something happened"); // 扩展后为 LOG_IMPL("INFO:" "Something happened")
避免语法错误:
在某些情况下,VA_OPT 可以用于避免宏展开导致的语法错误,比如避免连续的运算符。例如,避免连续的 && 或 || 运算符:
#define AND(...) __VA_ARGS__ __VA_OPT__(&&) __VA_ARGS__
bool result = AND(a > b) AND(c < d); // 扩展后为 bool result = (a > b) && (c < d);
这些场景展示了 VA_OPT 的灵活性,它不仅限于插入逗号,还可以用来控制宏展开时的任何文本或符号的条件性插入。
你还能想到什么__VA_OPT__
的巧妙用法?来评论区留言吧!