MSVC 兼容性

当 Clang 为 Windows 编译 C++ 代码时,它会尝试与 MSVC 保持兼容。兼容性包含多个方面。

首先,Clang 尝试与 MSVC 保持 ABI 兼容,这意味着 Clang 编译的代码应该能够成功链接到 MSVC 编译的代码。然而,C++ ABI 非常庞大且复杂,Clang 对 MSVC C++ ABI 的支持仍在进行中。如果您不需要 MSVC ABI 兼容性,或者不想使用微软的 C 和 C++ 运行时库,mingw32 工具链可能更适合您的项目。

其次,Clang 实现了许多 MSVC 语言扩展,例如 __declspec(dllexport) 和一些编译指示。这些通常由 -fms-extensions 控制。

第三,MSVC 接受一些 Clang 通常会诊断为无效的 C++ 代码。当这些结构出现在广泛包含的系统头文件中时,Clang 会尝试恢复并继续编译用户的程序。大多数解析和语义兼容性调整由 -fms-compatibility-fdelayed-template-parsing 控制,它们仍在开发中。

最后,还有 clang-cl,这是一个用于 clang 的驱动程序,它试图与 MSVC 的 cl.exe 保持兼容。

ABI 特性

主要 ABI 影响 C++ 特性的状态

  • 记录布局:已完成。我们使用模糊测试进行了测试,并修复了所有已知的错误。

  • 类继承:基本完成。这涵盖了您期望的所有标准面向对象特性:虚拟方法继承、多重继承和虚拟继承。偶尔我们会发现我们的表不兼容的错误,但这已经得到很好的控制。此功能也经过了模糊测试。

  • 名称修饰:进行中。每个新的 C++ 功能通常都需要自己的修饰。例如,成员指针模板参数具有有趣且独特的修饰。幸运的是,错误的修饰通常不会导致运行时错误。具有错误修饰的非内联函数通常会导致链接错误,这些错误相对容易诊断。内联函数和模板的错误修饰会导致最终映像中出现多个副本。C++ 标准要求这些地址相等,但很少有程序依赖于此。

  • 成员指针:基本完成。标准 C++ 成员指针已完全实现,应该与 ABI 兼容。 both #pragma pointers_to_members and the /vm flags are supported. 但是,MSVC 支持扩展来允许创建 指向虚拟基类成员的指针。Clang 尚未支持此功能。

  • 调试信息:基本完成。如果传递了 /Z7/Zi,Clang 会发出相对完整的 CodeView 调试信息。微软的 link.exe 会将 CodeView 调试信息转换为 PDB,可在 Windows 调试器和其他使用 PDB 文件的工具中使用,例如 ETW。关于向 lld 教授 CodeView 和 PDB 的知识的工作正在进行中。

  • RTTI:已完成。RTTI 数据结构的生成已完成,以及对 /GR 标志的支持。

  • C++ 异常:基本完成。对 C++ 异常 (try / catch / throw) 的支持已为 x86 和 x64 实现。我们的实现已经过充分测试,但我们偶尔还是会收到一些奇怪的错误报告。C++ 异常规范被忽略,但这与 Visual C++ 一致

  • 异步异常 (SEH):部分。结构化异常 (__try / __except / __finally) 主要在 x86 和 x64 上运行。LLVM 不会模拟异步异常,因此目前无法捕获在与捕获的 __try 相同的帧中生成的异步异常。

  • 线程安全初始化本地静态变量:已完成。MSVC 2015 通过打破 ABI 添加了对这些变量的线程安全初始化的支持。我们与 MSVC 2013 和 2015 ABI 对静态本地变量的 ABI 兼容。

  • Lambda 表达式:基本完成。Clang 与微软对 lambda 表达式的实现兼容,除了为不同调用约定提供转换为函数指针的重载。但是,微软的扩展是非标准的。

模板实例化和名称查找

MSVC 允许在类模板中使用许多 Clang 历史上拒绝的无效结构。为了解析广泛分布的 Active Template Library (ATL) 和 Windows Runtime Library (WRL) 等库的头文件,在 Windows 上的 Clang 中,一些模板规则已被放宽或扩展。

第一个主要的语义差异是,MSVC 似乎会将类模板中所有内联方法体体的解析和分析推迟到实例化时。在 Windows 上,默认情况下,Clang 会尝试效仿。此行为由 -fdelayed-template-parsing 标志控制。虽然 Clang 会延迟解析方法体,但它仍然会在模板参数替换之前解析方法体,而 MSVC 不会这样做。以下兼容性调整对于在这些情况下解析模板是必要的。

MSVC 允许在依赖基类中进行一些名称查找。即使在其他平台上,这也一直是 Clang 用户 经常问到的问题。依赖基类是一个依赖于模板参数值的基类。Clang 在解析模板时无法看到依赖基类中的任何名称,因此用户有时需要使用 typename 关键字来帮助解析器。在 Windows 上,Clang 会尝试遵循正常的查找规则,但如果查找失败,它会假设用户想要在依赖基类中查找名称。在解析以下程序时,Clang 会恢复,就好像用户编写了注释掉的代码一样

template <typename T>
struct Foo : T {
  void f() {
    /*typename*/ T::UnknownType x =  /*this->*/unknownMember;
  }
};

恢复后,Clang 会警告用户此代码不符合标准,并发出提示,建议如何解决问题。

截至撰写本文时,Clang 能够编译一个简单的 ATL hello world 应用程序。仍然存在解析用于现代 Windows 8 应用程序的 WRL 头文件的问题,但这些问题应该很快得到解决。

__forceinline 行为

__forceinline 的行为类似于 [[clang::always_inline]]。无论优化级别如何,始终尝试内联。

这与 MSVC 不同,在 MSVC 中,__forceinline 仅在启用内联扩展后才会生效,这允许任何标记为隐式或显式 inline__forceinline 的函数进行扩展。因此,当优化级别为 /Od 时,标记为 __forceinline 的函数将被扩展,而在 MSVC 中,__forceinline/Od 下不会被扩展。

SIMD 和指令集内联函数行为

Clang 遵循 GCC 的内联函数模型,而不是 MSVC 模型。目前没有计划支持 MSVC 模型。

MSVC 内联函数始终发出内联函数模拟的机器指令,无论指定了哪些编译时选项。例如,__popcnt 始终发出 x86 popcnt 指令,即使编译器没有启用自行发出 popcnt 的选项。

在使用 MSVC 编译的代码需要在 clang 上重新构建的两种常见情况下,假设这些示例仅使用 -msse2 构建,因此我们在编译时没有内联函数。

unsigned PopCnt(unsigned v) {
  if (HavePopCnt)
    return __popcnt(v);
  else
    return GenericPopCnt(v);
}
__m128 dot4_sse3(__m128 v0, __m128 v1) {
  __m128 r = _mm_mul_ps(v0, v1);
  r = _mm_hadd_ps(r, r);
  r = _mm_hadd_ps(r, r);
  return r;
}

Clang 期望您拥有目标特性的编译时支持,-msse3-mpopcnt,或者将函数标记为预期的目标特性,或者使用间接调用进行运行时检测。

__attribute__((__target__("sse3"))) __m128 dot4_sse3(__m128 v0, __m128 v1) {
  __m128 r = _mm_mul_ps(v0, v1);
  r = _mm_hadd_ps(r, r);
  r = _mm_hadd_ps(r, r);
  return r;
}

SSE3 点积可以通过使用 SSE3 支持构建翻译单元或使用 __target__ 以 SSE3 支持编译特定函数来轻松修复。

unsigned PopCnt(unsigned v) {
  if (HavePopCnt)
    return __popcnt(v);
  else
    return GenericPopCnt(v);
}

上面的 PopCnt 示例必须更改为适应 clang。如果我们用 __target__(“popcnt”) 标记函数,那么编译器可以随意发出 popcnt,而我们不希望这样。虽然这在我们的小示例中不是问题,但在具有围绕内联函数的周围代码的更大函数中,这是一个问题。类似的推理适用于使用 -mpopcnt 编译翻译单元。我们必须将每个分支拆分为可以间接调用的单独函数,而不是直接使用内联函数。

__attribute__((__target__("popcnt"))) unsigned hwPopCnt(unsigned v) { return __popcnt(v); }
unsigned (*PopCnt)(unsigned) = HavePopCnt ? hwPopCnt : GenericPopCnt;
__attribute__((__target__("popcnt"))) unsigned hwPopCnt(unsigned v) { return __popcnt(v); }
unsigned PopCnt(unsigned v) {
  if (HavePopCnt)
    return hwPopCnt(v);
  else
    return GenericPopCnt(v);
}

在上面的示例中,hwPopCnt 不会内联到 PopCnt 中,因为 PopCnt 没有 popcnt 目标特性。在执行实际工作的更大函数中,函数调用开销可以忽略不计。但是,在我们这个 popcnt 示例中,存在函数调用开销。clang 中没有与这种特定 MSVC 行为类似的模拟。

对于 clang,我们实际上必须自己创建调度函数以实现每个特定实现。

SIMD 向量类型

Clang 的 simd 向量类型是内置类型,而不是 MSVC 中的用户定义类型。这确实会带来一些可观察的行为变化。在下面的示例中,我们将关注 x86 __m128 类型,但这些语句适用于所有向量类型,包括 ARM 的 float32x4_t

向量类型没有可以访问的成员。在 clang 中,向量类型不是结构体。您不能使用 __m128.m128_f32[0] 访问 __m128 的第一个元素。这也意味着像 __m128{ >{ 0.0f, 0.0f, 0.0f, 0.0f } } 这样的结构初始化在 clang 中无法编译。

由于向量类型是内置类型,因此 clang 本地实现了这些类型上的运算符。

#ifdef _MSC_VER
__m128 operator+(__m128 a, __m128 b) { return _mm_add_ps(a, b); }
#endif

上面的代码将无法编译,因为重载的“operator+”必须至少包含一个类或枚举类型的参数。 您需要修复此代码以进行检查 #if defined(_MSC_VER) && !defined(__clang__)

由于 __m128 在 clang 中不是类类型,因此模板定义后的任何重载都不会被考虑。

template<class T>
void foo(T) {}

template<class T>
void bar(T t) {
  foo(t);
}

void foo(__m128) {}

int main() {
  bar(_mm_setzero_ps());
}

对于 MSVC,foo(__m128) 将被选中,但对于 clang,foo<__m128>() 将被选中,因为在 clang 上 __m128 是一个内置类型。

一般来说,关键点是 __m128 在 clang 上是内置类型,而在 MSVC 上是类类型。