MSVC 新预处理器概述
Visual Studio 2015 使用不符合 C++ 或 C99 标准的传统预处理器。 从 Visual Studio 2019 版本 16.5 开始,对 C++20 标准的新预处理器支持功能完整。 可以使用 /Zc:preprocessor 编译器开关获取这些更改。 从 Visual Studio 2017 版本 15.8 及更高版本开始,可使用 /experimental:preprocessor 编译器开关获得新预处理器的实验版本。 提供了有关在 Visual Studio 2017 和 Visual Studio 2019 中使用新预处理器的详细信息。 若要查看 Visual Studio 首选项的文档,请使用“版本”选择器控件。 它位于此页面上目录表的顶部。
我们正在更新 Microsoft C++ 预处理器以提高标准一致性,修复长期 bug,并更改一些官方未定义的行为。 我们还添加了新的诊断来针对宏定义中的错误发出警告。
从 Visual Studio 2019 版本 16.5 开始,对 C++20 标准的预处理器支持功能完整。 可以使用 /Zc:preprocessor 编译器开关获取这些更改。 从 Visual Studio 2017 版本 15.8 开始,早期版本中提供了新预处理器的实验版本。 可以通过使用 /experimental:preprocessor 编译器开关来启用该版本。 默认预处理器行为与以前的版本相同。
新的预定义宏
可以检测在编译时使用的预处理器。 检查预定义宏 _MSVC_TRADITIONAL
的值,判断是否正在使用传统预处理器。 此宏由支持它的编译器版本无条件设置,与调用哪个预处理器无关。 对于传统预处理器,其值为 1。 对于符合要求的预处理器,该值为 0。
#if !defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL
// Logic using the traditional preprocessor
#else
// Logic using cross-platform compatible preprocessor
#endif
新预处理器中的行为更改
新预处理器的初始工作侧重于使所有宏扩展都符合标准。 它允许将 MSVC 编译器与当前被传统行为阻止的库一起使用。 我们在实际项目中测试了更新后的预处理器。 以下是我们发现的一些更常见的中断性变更:
宏注释
传统的预处理器基于字符缓冲区,而不是预处理器标记。 它允许异常行为,例如以下预处理器注释技巧,该技巧在符合标准的预处理器下不起作用:
#if DISAPPEAR
#define DISAPPEARING_TYPE /##/
#else
#define DISAPPEARING_TYPE int
#endif
// myVal disappears when DISAPPEARING_TYPE is turned into a comment
DISAPPEARING_TYPE myVal;
符合标准的修复是在相应的 #ifdef/#endif
指令中声明 int myVal
:
#define MYVAL 1
#ifdef MYVAL
int myVal;
#endif
L#val
传统预处理器错误地将字符串前缀合并到字符串化运算符 (#) 运算符的结果中:
#define DEBUG_INFO(val) L"debug prefix:" L#val
// ^
// this prefix
const wchar_t *info = DEBUG_INFO(hello world);
在这种情况下,L
前缀是不必要的,因为相邻字符串字面量无论如何都在宏扩展之后合并。 向后兼容的修复是更改定义:
#define DEBUG_INFO(val) L"debug prefix:" #val
// ^
// no prefix
在将参数“字符串化”为宽字符串文字的便利宏中也发现了相同的问题:
// The traditional preprocessor creates a single wide string literal token
#define STRING(str) L#str
可以通过多种方式修复此问题:
使用
L""
和#str
的字符串串联来添加前缀。 相邻的字符串字面量在宏扩展后合并:#define STRING1(str) L""#str
在使用附加宏扩展对
#str
进行字符串化后添加前缀#define WIDE(str) L##str #define STRING2(str) WIDE(#str)
使用串联运算符
##
合并标记。 未指定##
和#
的操作顺序,但在这种情况下所有编译器似乎都在##
之前评估#
运算符。#define STRING3(str) L## #str
无效警告 ##
当标记粘贴运算符 (##) 未生成单个有效预处理标记时,则表明行为未定义。 传统预处理器无法合并标记且不会发出提示。 新预处理器与大多数其他编译器的行为匹配,并会发出诊断。
// The ## is unnecessary and does not result in a single preprocessing token.
#define ADD_STD(x) std::##x
// Declare a std::string
ADD_STD(string) s;
可变参数宏中的逗号省略
传统的 MSVC 预处理器始终会移除空 __VA_ARGS__
替换内容前的逗号。 新的预处理器更严密地遵循其他常用跨平台编译器的行为。 要移除逗号,必须缺少可变参数(不仅仅为空),并且必须用 ##
运算符标记。 请考虑以下示例:
void func(int, int = 2, int = 3);
// This macro replacement list has a comma followed by __VA_ARGS__
#define FUNC(a, ...) func(a, __VA_ARGS__)
int main()
{
// In the traditional preprocessor, the
// following macro is replaced with:
// func(10,20,30)
FUNC(10, 20, 30);
// A conforming preprocessor replaces the
// following macro with: func(1, ), which
// results in a syntax error.
FUNC(1, );
}
在以下示例中,在对 FUNC2(1)
的调用中,要调用的宏中缺少可变参数。 在对 FUNC2(1, )
的调用中,可变参数为空,但没有缺失(注意参数列表中的逗号)。
#define FUNC2(a, ...) func(a , ## __VA_ARGS__)
int main()
{
// Expands to func(1)
FUNC2(1);
// Expands to func(1, )
FUNC2(1, );
}
在即将推出的 C++20 标准中,已通过添加 __VA_OPT__
解决了此问题。 从 Visual Studio 2019 版本 16.5 开始,提供对 __VA_OPT__
的新预处理器支持。
C++20 可变参数宏扩展
新的预处理器支持 C++20 可变参数宏参数省略:
#define FUNC(a, ...) __VA_ARGS__ + a
int main()
{
int ret = FUNC(0);
return ret;
}
此代码不符合 C++20 标准之前的标准。 在 MSVC 中,新的预处理器将此 C++20 行为扩展到较低的语言标准模式(/std:c++14
、/std:c++17
)。 此扩展与其他主要跨平台 C++ 编译器的行为匹配。
宏参数已“解压缩”
在传统的预处理器中,如果某个宏将它的一个参数转发给另一个依赖宏,那么该参数在插入时不会“解压缩”。 此优化通常不易察觉,但它可能导致异常行为:
// Create a string out of the first argument, and the rest of the arguments.
#define TWO_STRINGS( first, ... ) #first, #__VA_ARGS__
#define A( ... ) TWO_STRINGS(__VA_ARGS__)
const char* c[2] = { A(1, 2) };
// Conforming preprocessor results:
// const char c[2] = { "1", "2" };
// Traditional preprocessor results, all arguments are in the first string:
// const char c[2] = { "1, 2", };
展开 A()
时,传统的预处理器会将 __VA_ARGS__
中打包的所有参数转发到 TWO_STRINGS 的第一个参数,从而将 TWO_STRINGS
的可变参数留空。 这导致 #first
的结果为“1, 2”而不仅仅是“1”。 如果严格按照相关步骤操作,你可能想知道传统预处理器扩展中 #__VA_ARGS__
的结果发生了什么:如果可变参数为空,它应该导致一个空字符串字面量 ""
。 一个单独的问题导致无法生成空字符串字面量标记。
重新扫描替换列表中的宏
替换宏后,将重新扫描生成的标记以查找要替换的其他宏标识符。 传统预处理器用于执行重新扫描的算法不符合要求,如以下基于实际代码的示例所示:
#define CAT(a,b) a ## b
#define ECHO(...) __VA_ARGS__
// IMPL1 and IMPL2 are implementation details
#define IMPL1(prefix,value) do_thing_one( prefix, value)
#define IMPL2(prefix,value) do_thing_two( prefix, value)
// MACRO chooses the expansion behavior based on the value passed to macro_switch
#define DO_THING(macro_switch, b) CAT(IMPL, macro_switch) ECHO(( "Hello", b))
DO_THING(1, "World");
// Traditional preprocessor:
// do_thing_one( "Hello", "World");
// Conforming preprocessor:
// IMPL1 ( "Hello","World");
虽然这个示例看起来是人为想象的,但我们已经在实际代码中看到了它。
要查看发生了什么,我们可以对以 DO_THING
开始的扩展进行细分:
DO_THING(1, "World")
扩展为CAT(IMPL, 1) ECHO(("Hello", "World"))
CAT(IMPL, 1)
扩展为IMPL ## 1
,后者扩展为IMPL1
- 现在,标记处于以下状态:
IMPL1 ECHO(("Hello", "World"))
- 预处理器发现类似于函数的宏标识符
IMPL1
。 由于它后面没有(
,因此不被视为类似函数的宏调用。 - 预处理器继续处理以下标记。 它发现调用了类似函数的宏
ECHO
:ECHO(("Hello", "World"))
(扩展为("Hello", "World")
) - 不再考虑将
IMPL1
用于扩展,因此扩展的完整结果为:IMPL1("Hello", "World");
要将宏修改为在新预处理器和传统预处理器下行为方式相同,请添加另一个间接层:
#define CAT(a,b) a##b
#define ECHO(...) __VA_ARGS__
// IMPL1 and IMPL2 are macros implementation details
#define IMPL1(prefix,value) do_thing_one( prefix, value)
#define IMPL2(prefix,value) do_thing_two( prefix, value)
#define CALL(macroName, args) macroName args
#define DO_THING_FIXED(a,b) CALL( CAT(IMPL, a), ECHO(( "Hello",b)))
DO_THING_FIXED(1, "World");
// macro expands to:
// do_thing_one( "Hello", "World");
16.5 之前的功能不完整
从 Visual Studio 2019 版本 16.5 开始,新的预处理器已针对 C++20 提供了完整的功能。 在 Visual Studio 的早期版本中,新的预处理器大多功能完整,但一些预处理器指令逻辑仍然回退到传统行为。 以下是 16.5 之前的 Visual Studio 版本中不完整功能的部分列表:
_Pragma
支持- C++20 功能
- Boost 阻塞 bug:预处理器常量表达式中的逻辑运算符未在版本 16.5 之前的新预处理器中完全实现。 在某些
#if
指令上,新的预处理器可以回退到传统的预处理器。 只有在扩展与传统预处理器不兼容的宏时,效果才明显。 生成 Boost 预处理器槽时,可能会发生这种情况。