今天多次在开源项目SerenityOS中见到两个以前很少见的与字符串相关的C++预处理特性,故记录如下(之前应该也遇到过,但印象不是很深,希望这次能够牢记)
首先看这段代码
#define ENUMERATE_SYSCALLS(S) \
S(yield) \
S(open) \
S(close) \
S(read) \
S(lseek) \
S(kill)
/* remaining syscalls omitted... */
namespace Syscall {
enum Function {
#undef __ENUMERATE_SYSCALL
#define __ENUMERATE_SYSCALL(x) SC_##x,
ENUMERATE_SYSCALLS(__ENUMERATE_SYSCALL)
#undef __ENUMERATE_SYSCALL
__Count
};
constexpr const char* to_string(Function function)
{
switch (function) {
#undef __ENUMERATE_SYSCALL
#define __ENUMERATE_SYSCALL(x) \
case SC_##x: \
return #x;
ENUMERATE_SYSCALLS(__ENUMERATE_SYSCALL)
#undef __ENUMERATE_SYSCALL
default:
break;
}
return "Unknown";
}
这段代码乍一看很容易理解,第一个宏ENUMERATE_SYSCALLS(S)
展开为一系列的S(系统调用名)
,至于其具体含义暂且不论。但第二个宏非常令人费解,什么是SC_##x
呢?
如果忽视这个问题继续分析这个宏定义,那么它的展开则非常简单,就是SC_##x,
。下一行用到了第一个宏,将S
替换为__ENUMERATE_SYSCALL
。
因此,这个宏的展开就是一系列的__ENUMERATE_SYSCALL(系统调用名)
,再展开一次,就变成一系列的SC_##系统调用名
。再结合外面的枚举定义的语句来看,这段代码定义了一系列的系统调用的枚举。
但转念一想,C++规定其标识符不可以出现除了_(某些编译器还支持$)之外的特殊符号。所以如果简单的展开为SC_##open
肯定是不对的。
将这个问题记在小本本上继续往下看…下面是一个函数constexpr const char* to_string(Function function)
,接受一个Function
类型的枚举变量,返回一个字面值字符串常量(constexpr)。
顾名思义,应该是将系统变量的枚举名转换成相应的字符串,那么这个转换具体是怎么实现的呢?通常来讲,我们应该这么写:
switch (function) {
case SC_yield:
return "yield";
case SC_open:
return "open";
/* ... */
default:
break;
}
然而系统调用的数量非常庞大,并且还会随着开发的进行不断增加,每次都要这样手动输入是非常麻烦的。下面我们就来分析第一段代码中是怎么实现这一目标的。
注意到,第一段代码又出现了SC_##x
,同时又出现了一个#x
。
查阅资料得知,在C++规范中,
出现在宏定义的替换列表中的标识符前的#将标识符进行替换,并将其用双引号包裹,也即将其转换为一个字符串字面值。所以第一段代码中的return #x
就会被转换成return "open"
等等。Cppreference网站的原文参考如下:
In function-like macros, a # operator before an identifier in the replacement-list runs the identifier through parameter replacement and encloses the result in quotes, effectively creating a string literal. In addition, the preprocessor adds backslashes to escape the quotes surrounding embedded string literals, if any, and doubles the backslashes within the string as necessary. All leading and trailing whitespace is removed, and any sequence of whitespace in the middle of the text (but not inside embedded string literals) is collapsed to a single space. This operation is called “stringification”. If the result of stringification is not a valid string literal, the behavior is undefined.
而两个标识符之间的##则会将这两个标识符连接起来,因此,所有的SC_##x
都会被展开为相应的SC_open
,SC_yield
等等。Cppreference网站的原文参考如下:
A ## operator between any two successive identifiers in the replacement-list runs parameter replacement on the two identifiers (which are not macro-expanded first) and then concatenates the result. This operation is called “concatenation” or “token pasting”. Only tokens that form a valid token together may be pasted: identifiers that form a longer identifier, digits that form a number, or operators + and = that form a +=. A comment cannot be created by pasting / and * because comments are removed from text before macro substitution is considered. If the result of concatenation is not a valid token, the behavior is undefined.
了解了以上两个符号的作用,上面的宏就很容易理解了,这样两个宏节约了非常多的代码量,值得学习。