为什么不能分发 C++ 二进制模块接口(BMI)
原因:不同编译器,不同编译参数会导致内部的 AST 不一致。
AST 不一致的来源有以下几个:
- 编译器提供的宏不一样,例如代码使用
__clang__或者_MSC_VER进行条件编译 - 有些库允许用户更改宏的定义以支持不同的功能,例如
NDEBUG宏和_UNICODE宏 - 不同 C++ 标准下,第三方库和标准库提供不同的功能
- 实现定义的内容可以被编译器选项控制,例如 -fexecution-charset 会改变基本字符串字面量的编码,-funsigned-char 会改变
char的符号性 - 编译器实现细节导致的定义不一致,例如不同编译器支持的 intrinsic 不一样,以及我为 Clang 实现的“糖类型”在 AST 可见但语言不可见(Clang内部有多个糖类型)
- 编译器支持的扩展不同,例如 MSVC 不支持
__int128_t,GCC 把__int128_t和_Bitint(128)实现为相同类型,而在 Clang 中是不同类型,如果使用is_same_v,这也会改变代码的行为
实际上以上所有产生不同 AST 的行为,在被 ODR 使用时(见下文)都违反 ODR,这不是模块的缺点,使用头文件时存在上述任何问题仍然有可能产生不同 AST 并且违反 ODR。
因此,分发 BMI 显然是天方夜谭,我不知道什么人在宣传 BMI 能分发或者生成 BMI 不能分发是某种缺点,他们显然对编译 C++ 一窍不通。
在不使用模块时,不同翻译单元对于“同一定义”有不同的 AST 不一定违反 ODR,因为它们不一定被 ODR 使用。
例如 __is_scoped_enum 这个 Clang 的 intrinsic 一定被常量求值,在多个翻译单元中它的值一定是相同的。使用模块后,模块会把 __is_scoped_enum 保留在 AST 里,被依赖它的模块使用(合并),此时,这个 BMI 就不能被不支持 __is_scoped_enum 的编译器(例如 MSVC 和 EDG)使用。