MSVC编译器预处理程序迈向标准化


[原文发表地址] MSVC Preprocessor Progress towards Conformance

[原文发表时间] 2018/07/06

为什么重写编译器预处理程序

 

最近我们发布了一篇关于C++标准完成度的一篇博客。 博客中提到, MSVC预处理程序正在进行大改动. 这样做的目的旨在提高编译器的语言标准, 解决一些因框架设计导致修复困难的一些久远的bug, 并提高可用性与诊断性。 除此之外, C++标准在某些预处理行为上并没有指定定义, 而且我们的传统行为与其他主要编译器有所不同。在这种情况下, 我们尽可能亲和当前的体系结构并使交互平台的一些库更加方便的支持MSVC。

 

如果你使用了一个依赖MSVC预处理程序的非标准传统行为的库, 你大可不用担心兼容性。更新后的预处理程序工作在/experimental:preprocessor开关下, 当完全实现时它将会转为/permissive-模式下一个含有/Zc:的默认开启开关。

 

我如何去使用

 

维护传统预处理程序的行为并继续作为编译器的默认行为。 从VS2017 Preview 3 release开启命令提示框, 使用/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

新的变动

 

第一个发布的实验预处理程序主要致力完成对C++标准支持相关的宏,使MSVC编译器可以最大化支持各种最新库。由于使用已更新的预处理程序,在测试实际产品项目中出现了一些不兼容,我们需要进行重新修改, 下面列出一些常见的修改方案.

行为1  宏注释

传统的预处理程序基于字符串缓冲区而不是预处理单词。 下面的代码通过宏注释的技巧将不会在更新后的标准化预处理程序中工作.

#if DISAPPEAR

#define DISAPPEARING_TYPE /##/

#else

#define DISAPPEARING_TYPE int

#endif

//当DISAPPEARING_TYPE变成注释时myVal会被注释掉

//为了符合标准,使用//将随后的代码包含// #if /#endif

DISAPPEARING_TYPE myVal;

行为2 [L#val]

 

传统的预处理程序将字符串前缀的合并不正确的处理为#操作符

 #define DEBUG_INFO(val) L”debug prefix:” L#val

//                                       ^

//                                       this prefix

这个例子中L前缀没有必要, 因为临近的字符串字面值在宏展开后总是会合并. 向后兼容的修复是改变宏定义为

#define DEBUG_INFO(val) L”debug prefix:” #val

//                                       ^

//                                       no prefix

这个问题也出现在在字符化参数转换为宽字符串的转换宏上.

//传统的预处理程序创建一个单独的宽字符串字面值词法
#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

行为3 无效的##警告

 

当##操作符无法形成一个单独的有效的预处理单元时, 即为未定义行为。传统的预处理程序无法继续合并标记。新的预处理程序将会与其他主要编译器行为保持一致并给出诊断.

//##是不必要的, 并不会成为一个单独的预处理语法单元
#define ADD_STD(x) std::##x
//声明一个std::string对象
ADD_STD(string) s;

行为4 省略变长宏中的逗号

考虑下面的例子
#define FUNC2(a, …) func(a , ## __VA_ARGS__)
int main()
{
// 变长参数在被调用的宏中丢弃The variadic argument is missing in the macro being evoked
// 逗号将会移除并替换为Comma will be removed and replaced with:
// func(1)
FUNC2(1);
// 变长参数为空,但不会丢弃, 注意参数列表中的逗号。 当宏被替换时逗号不会移除
// func(1, )
FUNC2(1, );
}

在即将到来的C++2a标准中, 这个问题通过添加__VA_OPT_解决, 这个特性暂时还没有实施.

行为5 宏参数未解包

在传统预处理程序中, 如果一个宏转发另一个宏依赖的参数, 这时参数被替换时不会解包。 通常这种优化会被忽视, 但是他可以导致不寻常的行为

// 在first参数和剩余参数外创建一个字符串
#define TWO_STRINGS( first, … ) #first, #__VA_ARGS__
#define A( … ) TWO_STRINGS(__VA_ARGS__)
const char* c[2] = { A(1, 2) };
// 标准的预处理程序导致:
// const char c[2] = { “1”, “2” };
// 传统的预处理程序导致所有的参数都在第一个字符串里
// const char c[2] = { “1, 2”, };

当展开A()时, 传统预处理程序转发所有__VA_ARGS_中打包的参数到TWO_STRINGS的第一个参数, 它使TWO_STRING的变长参数为空. #first变为"1,2" 而不是仅仅只是"1".如果仔细跟踪, 你可能好奇#_VA_ARGS_在传统预处理程序中的结果是什么; 如果变长参数为空它的值为空字符串字面值"".由于另外一个问题, 空字符串字面值没有生成.

 

行为6 重新扫描宏里面的替换列表

在宏被替换后, 影响的标识将会为需要被替换的附加宏标识符重新扫描。 下面的代码里,传统预处理程序在做重新扫描时使用的算法并不符合语言标准.

#define CAT(a,b) a ## b
#define ECHO(…) __VA_ARGS__
// IMPL1 和 IMPL2 为实现的细节
#define IMPL1(prefix,value) do_thing_one( prefix, value)
#define IMPL2(prefix,value) do_thing_two( prefix, value)
// 宏将会根据Macro_switch的值选择相应的展开方式
#define DO_THING(macro_switch, b) CAT(IMPL, macro_switch) ECHO(( “Hello”, b))
DO_THING(1, “World”);
// 传统的预处理程序
// do_thing_one( “Hello”, “World”);
// 标准化的预处理程序
// IMPL1 ( “Hello”,”World”);

虽然这个例子有些刻意,但是我们针对真实产品代码测试预处理程序时遇到几次这样的问题. 我们分解DO_THING的展开式来看一下发生了什么.

DO_THING(1, “World”)— >
CAT(IMPL, 1) ECHO((“Hello”, “World”))

然后CAT被展开为

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 和 IMPL2 为宏的实现细节
#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”);
// 宏展开为:
// do_thing_one( “Hello”, “World”);

后续

预处理程序重写还没有完成;我们会在体验模式下进行修改, 并修复早期使用过程中出现的bug.

  • 一些预处理指令将会与传统行为不一样
  • 支持_Pragma
  • C++20特性
  • 进一步提升诊断能力
  • 使用新的编译选项/E /P来控制输出
  • 阻碍Boost的bug
    • 预处理常量表达式当中的逻辑操作符在新的预处理程序中还没有完全实现, 所以在#if指令上逻辑操作符会回退到传统的预处理程序行为。 当不匹配传统预处理的宏展开时, 回退行为较为明显, 在生成boost预处理程序插槽时也会出现这种情况.

结语

欢迎大家下载VS2017 15.8预览版并尝试使用所有实验性的新特性. 和往常一样, 我们欢迎你们的反馈。可以通过下面的留言或者电邮(visualcpp@microsoft.com). 如果你遇到其他VS2017 MSVC编译器的问题, 请通过Help > Report A Problem in the product 或者Developer Community 告诉我们.
通过USERVOICE可以让我们了解你的建议, 你也可以通过Twitter VISUAL C 与Fackbook msftvisualCpp 找到我们.