语言兼容性
Clang 努力做到既符合当前的语言标准(包括 C11 和 C++11),又实现其他编译器中广泛使用的许多扩展,以便大多数正确的代码在使用 Clang 编译时“正常工作”。但是,Clang 比其他流行的编译器更严格,并且可能会拒绝其他编译器允许的错误代码。此页面记录了 Clang 中常见的兼容性和可移植性问题,以帮助您了解和修复 Clang 发出错误消息时代码中的问题。
C 兼容性
C99 内联函数
默认情况下,Clang 以 GNU C17 模式构建 C 代码,因此它使用 inline
关键字的标准 C99 语义。这些语义不同于 GNU C89 模式中的语义,后者是 5.0 之前的 GCC 版本中的默认模式。例如,考虑以下代码
inline int add(int i, int j) { return i + j; } int main() { int i = add(4, 5); return i; }
在 C99 中,inline
表示函数的定义仅用于内联,并且在程序的别处还有另一个(没有 inline
的)定义。这意味着此程序不完整,因为如果 add
未内联(例如,在不进行优化的情况下编译时),那么 main
将对该其他定义有一个未解析的引用。因此,我们将得到一个(正确的)链接时错误,如下所示
Undefined symbols: "_add", referenced from: _main in cc-y1jXIr.o
相比之下,GNU C89 模式(在旧版本的 GCC 中默认使用)是 C89 标准加上许多扩展。C89 没有 inline
关键字,但 GCC 识别它作为扩展并将其视为对优化器的提示。
有几种方法可以解决此问题
- 将
add
更改为static inline
函数。如果只有一个翻译单元需要使用该函数,这通常是正确的解决方案。static inline
函数始终在翻译单元内解析,因此您不必在程序的别处添加函数的非inline
定义。 - 从
add
的此定义中删除inline
关键字。inline
关键字对于函数内联不是必需的,也不能保证它会被内联。一些编译器完全忽略它。Clang 将其视为程序员的轻微建议。 - 在程序的别处提供
add
的外部(非inline
)定义。这两个定义必须等效! - 通过将
-std=gnu89
添加到 Clang 选项集中,在 GNU C89 方言中进行编译。仅当程序源代码无法更改或程序还依赖于无法更改的其他 C89 特定行为时才建议使用此选项。
所有这些仅适用于 C 代码;inline
在 C++ 中的含义与它在 GNU89 或 C99 中的含义大不相同。
“缺失”的向量 __builtin 函数
Intel 和 AMD 手册记录了数字“<*mmintrin.h>”头文件,它定义了访问 X86 CPU 上向量操作的标准化 API。这些函数的名称类似于_mm_xor_ps和_mm256_addsub_pd. 编译器可以自行决定实现这些函数。由于 Clang 支持一组优秀的 原生向量操作,因此 Clang 头文件使用原生向量操作来实现这些接口。
相反,GCC 主要将这些函数实现为与内置函数调用的一对一映射,例如__builtin_ia32_paddw128. 这些内置函数是 GCC 的内部实现细节,不可移植到 Intel 编译器、Microsoft 编译器或 Clang。如果您收到提及这些的构建错误,则修复很简单:切换到 *mmintrin.h 函数。
对于 ARM 和 PowerPC 架构,NEON 和 Altivec 分别也出现相同问题。对于这些,请确保使用 <arm_neon.h> 和 <altivec.h> 头文件。
对于 x86 架构,此 脚本 应该有助于手动迁移过程。它将就地重写您的源文件,以使用 API 而不是内置函数调用。只需按以下方式调用它
builtins.py *.c *.h
它将重写当前目录中的所有 .c 和 .h 文件,以使用 API 调用而不是类似于__builtin_ia32_paddw128.
左值转换
旧版本的 GCC 允许将赋值左侧转换为不同的类型。Clang 在类似代码上产生错误,例如:
lvalue.c:2:3: error: assignment to cast is illegal, lvalue casts are not supported (int*)addr = val; ^~~~~~~~~~ ~
要解决此问题,请将转换移至右侧。在本例中,可以使用
addr = (float *)val;
跳转到内部__block变量范围
Clang 禁止跳转到__block变量的范围。标记为__block的变量需要特殊的运行时初始化。跳转到__block变量的范围会绕过此初始化,使变量的元数据处于无效状态。考虑以下代码片段
int fetch_object_state(struct MyObject *c) { if (!c->active) goto error; __block int result; run_specially_somehow(^{ result = c->state; }); return result; error: fprintf(stderr, "error while fetching object state"); return -1; }
GCC 接受此代码,但它生成的代码在 result
超出范围时通常会崩溃,如果采取了跳转。(此错误可能无法检测到,因为它在堆栈新鲜时(即仍为零)通常不会崩溃。)因此,Clang 使用硬错误拒绝此代码
t.c:3:5: error: goto into protected scope goto error; ^ t.c:5:15: note: jump bypasses setup of __block variable __block int result; ^
修复方法是重写代码,使其不再需要跳转到__block变量的范围,例如通过限制该范围
{ __block int result; run_specially_somehow(^{ result = c->state; }); return result; }
未初始化__block变量
在以下示例代码中,x变量在定义之前使用
int f0() { __block int x; return ^(){ return x; }(); }
由于实现的偶然性,GCC 和 llvm-gcc 无意中始终将__block变量初始化为零。但是,任何依赖于此行为的程序都依赖于未指定的编译器行为。程序必须在使用本地块变量之前显式地初始化所有本地块变量,就像其他本地变量一样。
Clang 不会将本地块变量初始化为零,并且依赖于这种行为的程序在使用 Clang 构建时很可能会中断。
内联汇编
通常,Clang 与 GCC 内联汇编扩展高度兼容,允许与 GCC 内联汇编相同的约束、修饰符和操作数集。
在使用集成汇编器的目标(如大多数 X86 目标)上,内联汇编将通过集成汇编器运行,而不是通过您的系统汇编器(通常是“gas”,即 GNU 汇编器)。LLVM 集成汇编器与 GAS 极其兼容,但在几个较小的位置,它更加挑剔,尤其是由于 GAS 的错误。
一个具体的例子是,汇编器拒绝没有后缀的不明确的 X86 指令。例如
asm("add %al, (%rax)"); asm("addw $4, (%rax)"); asm("add $4, (%rax)");
Clang 和 GAS 都接受第一个指令:因为第一个指令使用 8 位%al寄存器作为操作数,因此很明显它是一个 8 位加法。第二个指令被两者接受,因为“w”后缀表示它是一个 16 位加法。最后一个指令被 GAS 接受,即使没有任何东西指定指令的大小(汇编器随机选择了 32 位加法)。由于它不明确,因此 Clang 使用以下错误消息拒绝该指令
<inline asm>:3:1: error: ambiguous instructions require an explicit suffix (could be 'addb', 'addw', 'addl', or 'addq') add $4, (%rax) ^
要解决此兼容性问题,请为指令添加显式后缀:这使您的代码更加清晰,并且与 GCC 和 Clang 兼容。
Objective-C 兼容性
super 的转换
GCC 将 super
标识符视为一个表达式,它可以(除其他外)被转换为不同的类型。Clang 将 super
视为上下文敏感的关键字,并且会拒绝对 super
进行类型转换
super.m:11:12: error: cannot cast 'super' (it isn't an expression) [(Super*)super add:4]; ~~~~~~~~^
要解决此问题,请删除类型转换,例如
[super add:4];
接口的大小
在使用“非脆弱”的 Objective-C ABI 时,Objective-C 类的尺寸可能会随着时间的推移而发生变化,因为实例变量会添加(或删除)。因此,在使用此 ABI 时,Clang 会拒绝对 Objective-C 类应用 sizeof
运算符
sizeof.m:4:14: error: invalid application of 'sizeof' to interface 'NSArray' in non-fragile ABI int size = sizeof(NSArray); ^ ~~~~~~~~~
依赖于 Objective-C 类大小的代码可能本身就已损坏,因为该大小实际上不是常量。要解决此问题,请使用 Objective-C 运行时 API 函数 class_getInstanceSize()
class_getInstanceSize([NSArray class])
内部 Objective-C 类型
GCC 允许使用指向内部 Objective-C 对象的指针,struct objc_object*, struct objc_selector*和struct objc_class*代替类型id, SEL和Class分别。Clang 将内部 Objective-C 结构视为实现细节,不会进行隐式转换
t.mm:11:2: error: no matching function for call to 'f' f((struct objc_object *)p); ^ t.mm:5:6: note: candidate function not viable: no known conversion from 'struct objc_object *' to 'id' for 1st argument void f(id x); ^
代码应该使用类型id, SEL和Class而不是内部类型。
@interface 或 @protocol 中的 C 变量
GCC 允许在 @interface
或 @protocol
声明中声明 C 变量。Clang 不允许在这些声明中出现变量声明,除非它们被标记为 extern
。
仍然可以在 @implementation 中声明变量。
@interface XX int a; // not allowed in clang int b = 1; // not allowed in clang extern int c; // allowed @end
C++ 兼容性
变长数组
GCC 和 C99 允许在运行时确定数组的大小。此扩展在标准 C++ 中不允许。但是,Clang 支持此类变长数组以与 GNU C 和 C99 程序兼容。
如果您不想使用此扩展,可以使用-Werror=vla来禁用它。有几种方法可以修复您的代码
- 如果您可以在编译时确定合理的上限,请用固定大小的数组替换变长数组;有时这就像更改int size = ...;为const int size = ...;(如果初始化程序是编译时常量);
- 使用std::vector或其他一些合适的容器类型;或者
- 使用new Type[]在堆上分配数组 - 只要记住delete[]它。
模板中的非限定查找
某些版本的 GCC 接受以下无效代码
template <typename T> T Squared(T x) { return Multiply(x, x); } int Multiply(int x, int y) { return x * y; } int main() { Squared(5); }
Clang 抱怨
my_file.cpp:2:10: error: call to function 'Multiply' that is neither visible in the template definition nor found by argument-dependent lookup return Multiply(x, x); ^ my_file.cpp:10:3: note: in instantiation of function template specialization 'Squared<int>' requested here Squared(5); ^ my_file.cpp:5:5: note: 'Multiply' should be declared prior to the call site int Multiply(int x, int y) { ^
C++ 标准指出,像 Multiply
这样的非限定名称以两种方式进行查找。
首先,编译器在写入名称的范围内进行 *非限定查找*。对于模板,这意味着查找是在定义模板的位置进行的,而不是在实例化模板的位置进行的。由于Multiply在此处尚未声明,因此非限定查找将找不到它。
其次,如果该名称像函数一样被调用,则编译器还会进行 *依赖于参数的查找*(ADL)。(有时非限定查找可以抑制 ADL;有关详细信息,请参阅 [basic.lookup.argdep]p3。)在 ADL 中,编译器查看对调用的所有参数的类型。当它找到一个类类型时,它会在该类的命名空间中查找该名称;结果是它在这些命名空间中找到的所有声明,加上来自非限定查找的声明。但是,编译器直到知道所有参数类型后才会进行 ADL。
在我们的例子中,Multiply使用依赖参数进行调用,因此 ADL 直到实例化模板后才会进行。在那时,这些参数都具有类型int,它不包含任何类类型,因此ADL不会在任何命名空间中查找。由于这两种查找形式都未找到Multiply的声明,因此代码无法编译。
以下是一个示例,这次使用重载运算符,它们遵循非常相似的规则。
#include <iostream> template<typename T> void Dump(const T& value) { std::cout << value << "\n"; } namespace ns { struct Data {}; } std::ostream& operator<<(std::ostream& out, ns::Data data) { return out << "Some data"; } void Use() { Dump(ns::Data()); }
同样,Clang 也会报错
my_file2.cpp:5:13: error: call to function 'operator<<' that is neither visible in the template definition nor found by argument-dependent lookup std::cout << value << "\n"; ^ my_file2.cpp:17:3: note: in instantiation of function template specialization 'Dump<ns::Data>' requested here Dump(ns::Data()); ^ my_file2.cpp:12:15: note: 'operator<<' should be declared prior to the call site or in namespace 'ns' std::ostream& operator<<(std::ostream& out, ns::Data data) { ^
与之前一样,非限定查找未找到名称为operator<<的任何声明。与之前不同的是,参数类型都包含类类型:其中一个是类模板类型的实例std::basic_ostream,另一个是类型ns::Data,我们在上面声明了它。因此,ADL将在命名空间std和ns中查找operator<<。由于其中一个参数类型在模板定义期间仍然是依赖的,因此在模板实例化期间使用之前不会完成ADL,这意味着operator<<我们希望它找到的声明已经存在。不幸的是,它是在全局命名空间中声明的,而不是ADL将要查找的任何命名空间中!
有两种方法可以解决这个问题
- 确保要调用的函数在可能调用它的模板之前声明。如果其任何参数类型都不包含类,这将是唯一的选择。你可以通过移动模板定义、移动函数定义或在模板之前添加函数的向前声明来实现这一点。
- 将函数移到与其参数之一相同的命名空间中,以便ADL应用。
有关参数依赖查找的更多信息,请参阅[basic.lookup.argdep]。有关模板中查找顺序的更多信息,请参阅[temp.dep.candidate]。
类模板的依赖基类中的非限定查找
某些版本的 GCC 接受以下无效代码
template <typename T> struct Base { void DoThis(T x) {} static void DoThat(T x) {} }; template <typename T> struct Derived : public Base<T> { void Work(T x) { DoThis(x); // Invalid! DoThat(x); // Invalid! } };Clang 正确地拒绝了它,并显示以下错误(当Derived最终实例化时)
my_file.cpp:8:5: error: use of undeclared identifier 'DoThis' DoThis(x); ^ this-> my_file.cpp:2:8: note: must qualify identifier to find this declaration in dependent base class void DoThis(T x) {} ^ my_file.cpp:9:5: error: use of undeclared identifier 'DoThat' DoThat(x); ^ this-> my_file.cpp:3:15: note: must qualify identifier to find this declaration in dependent base class static void DoThat(T x) {}正如我们在上面所说的,像DoThis和DoThat这样的非限定名称是在模板Derived定义时查找的,而不是在实例化时查找的。当我们在类中查找名称时,我们通常会查看基类。但是,我们无法查看基类Base<T>,因为它的类型取决于模板参数T,因此标准规定我们应该忽略它。有关详细信息,请参阅 [temp.dep]p3。
正如 Clang 所提示的,解决方法是在调用前加上this->:
void Work(T x) { this->DoThis(x); this->DoThat(x); }告诉编译器我们想要一个类成员。或者,你可以告诉编译器确切的查找位置
void Work(T x) { Base<T>::DoThis(x); Base<T>::DoThat(x); }无论方法是静态的还是非静态的,这都适用,但要小心:如果DoThis是虚函数,以这种方式调用它将绕过虚调度!
模板中的不完整类型
以下代码无效,但编译器可以接受它
class IOOptions; template <class T> bool read(T &value) { IOOptions opts; return read(opts, value); } class IOOptions { bool ForceReads; }; bool read(const IOOptions &opts, int &x); template bool read<>(int &);标准规定,如果类型不依赖于模板参数,则在模板定义时必须完整,如果它们影响程序的行为。但是,标准还规定,编译器可以自由地不执行此规则。大多数编译器在一定程度上执行此规则;例如,在 GCC 中,编写opts.ForceReads上面的代码将是一个错误。在 Clang 中,我们认为一致地执行此规则可以让我们提供更好的体验,但不幸的是,这也意味着我们拒绝了其他编译器接受的一些代码。
我们在这里以非常不精确的术语解释了规则;有关详细信息,请参阅[temp.res]p8。
没有有效实例化的模板
以下代码包含一个拼写错误:程序员的意思是init(),但写成了innit()。
template <class T> class Processor { ... void init(); ... }; ... template <class T> void process() { Processor<T> processor; processor.innit(); // <-- should be 'init()' ... }不幸的是,我们无法在看到它时立即标记这个错误:在模板中,我们不允许对“依赖类型”做出假设,比如Processor<T>。假设稍后在这个文件中,程序员添加了Processor的显式特化,如下所示
template <> class Processor<char*> { void innit(); };现在程序将正常工作——只要程序员只使用process()来实例化,其中T = char*!这就是为什么在模板实例化之前很难,有时甚至不可能诊断模板定义中的错误的原因。
标准规定,没有有效实例化的模板是非法的。Clang 试图在定义时尽可能多地进行检查,而不是在实例化时进行检查:这不仅会产生更清晰的诊断信息,还会在使用预编译头文件时大幅提高编译时间。这种理念的缺点是,Clang 有时无法处理文件,因为这些文件包含不再使用的错误模板。解决方案很简单:由于代码未使用,只需将其删除即可。
类类型 const 变量的默认初始化需要用户定义的默认构造函数
如果class或struct没有用户定义的默认构造函数,则 C++ 不允许你像这样默认构造它的const实例 ([dcl.init],p9)
class Foo { public: // The compiler-supplied default constructor works fine, so we // don't bother with defining one. ... }; void Bar() { const Foo foo; // Error! ... }要解决此问题,可以为类定义一个默认构造函数
class Foo { public: Foo() {} ... }; void Bar() { const Foo foo; // Now the compiler is happy. ... }即将发布的 C++ 标准变更预计将弱化此规则,使其仅在编译器提供的默认构造函数会使成员未初始化时才适用。Clang 从 3.8 版本开始实施更宽松的规则。
参数名称查找
某些版本的 GCC 允许在 C++ 代码中的函数原型中重新声明函数参数名称,例如
void f(int a, int a);
Clang 会诊断此错误(其中参数名称已重新声明)。要解决此问题,请重命名其中一个参数。
C++11 兼容性
已删除的特殊成员函数
在 C++11 中,在类中显式声明移动构造函数或移动赋值运算符将删除复制构造函数和复制赋值运算符的隐式声明。此变更是在 C++11 标准化过程的后期提出的,因此 C++11 的早期实现(包括 3.0 之前的 Clang、4.7 之前的 GCC 以及 Visual Studio 2010)未实施此规则,导致它们接受此非法的代码
struct X { X(X&&); // deletes implicit copy constructor: // X(const X&) = delete; }; void f(X x); void g(X x) { f(x); // error: X has a deleted copy constructor }
这会影响一些早期的 C++11 代码,包括 Boost 的流行的shared_ptr(直到 1.47.0 版)。Boost 的shared_ptr的修复程序在此处提供.
Objective-C++ 兼容性
隐式向下转换
由于其实现中的错误,GCC 允许在调用函数时将 Objective-C 指针(从基类到派生类)隐式向下转换。这种代码本质上是不安全的,因为对象可能实际上不是派生类的实例,并且会被 Clang 拒绝。例如,给定以下代码
@interface Base @end @interface Derived : Base @end void f(Derived *p); void g(Base *p) { f(p); }
Clang 会产生以下错误
downcast.mm:6:3: error: no matching function for call to 'f' f(p); ^ downcast.mm:4:6: note: candidate function not viable: cannot convert from superclass 'Base *' to subclass 'Derived *' for 1st argument void f(Derived *p); ^
如果向下转换实际上是正确的(例如,因为代码已经检查了对象是否具有适当的类型),请添加显式转换
f((Derived *)base);
使用 class
作为属性名
在 C 和 Objective-C 中,class
是一个普通标识符,可用于命名字段、ivar、方法等。在 C++ 中,class
是一个关键字。为了与现有代码兼容,Clang 允许在 Objective-C++ 中将 class
用作方法选择器的一部分,但这不扩展到语言的其他任何部分。特别是,不可能在 Objective-C++ 中使用属性点语法,属性名称为 class
,因此以下代码将无法解析
@interface I { int cls; } + (int)class; @end @implementation I - (int) Meth { return I.class; } @end
改为使用显式消息发送语法,即 [I class]
。