Hexagon LLVM编译架构介绍(20)
6.2 C++兼容性
6.2.1 变长数组
GCC 和 C99 允许在运行时确定数组的大小。 标准 C++ 中不允许此扩展名。 但是,为了与 GNU C 和 C99 程序兼容,LLVM 在非常有限的情况下支持此类可变长度数组:
- 变长数组的元素类型必须是“普通旧数据”(POD) 类型,这意味着它不能有任何用户声明的构造函数或析构函数、任何基类或任何非POD 类型的成员。 所有 C 类型都是 POD 类型。
- 变长数组不能用作非类型模板参数的类型。
如果您的代码以 LLVM 不支持的方式使用可变长度数组,则有几种方法可以修复您的代码:
-
如果可以在编译时确定合理的上限,则将变长数组替换为固定大小的数组; 有时这就像改变 int size = …; 一样简单。 const int size = …; (如果初始化程序是编译时常量);
-
使用 std::vector 或其他一些合适的容器类型; 或者
-
在堆上分配数组而不是使用 new Type[] – 只需记住delete [] 它。
6.2.2 模板中的非限定查找
某些版本的 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);
}
LLVM 使用以下消息标记此代码:
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 中,编译器查看调用的所有参数的类型。 当它找到一个类类型时,它会在该类的命名空间中查找名称; 结果是它在这些命名空间中找到的所有声明,以及来自非限定查找的声明。 但是,编译器在知道所有参数类型之前不会执行 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());
}
同样,LLVM 使用以下消息标记此代码:
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 中查找运算符<<。 因为在模板定义期间参数类型之一仍然是依赖的,所以在 Use 期间实例化模板之前不会执行 ADL,这意味着已经声明了它应该找到的 operator<<。 不幸的是,它是在全局命名空间中声明的,而不是在 ADL 将要查找的任何一个命名空间中!
有两种方法可以解决这个问题:
- 确保您要调用的函数在可能调用它的模板之前声明。 如果它的参数类型都不包含类,这是唯一的选择。 您可以通过移动模板定义、移动函数定义或在模板之前添加函数的前向声明来完成此操作。
- 将该函数移动到与其参数之一相同的命名空间中,以便应用 ADL。
6.2.3 对类模板的依赖基础的非限定查找
某些版本的 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!
}
};
LLVM 正确拒绝此代码并出现以下错误(当 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) {}
如第 6.2.2 节所述,在定义模板 Derived 时查找诸如 DoThis 和 DoThat 之类的非限定名称,而不是在实例化时查找。 在查找类中使用的名称时,我们通常会查看基类。 但是,我们不能查看基类 Base 因为它的类型取决于模板参数 T,所以标准说我们应该忽略它。
正如 LLVM 所指出的,修复是通过在调用前加上 this-> 来告诉编译器我们想要一个类成员:
void Work(T x) {
this->DoThis(x);
this->DoThat(x);
}
或者,您可以告诉编译器确切的位置:
void Work(T x) {
Base<T>::DoThis(x);
Base<T>::DoThat(x);
}
无论方法是否是静态的,这都有效,但要小心:如果 DoThis 是virtual,以这种方式调用它会绕过virtual dispatch!
6.2.4 模板中的不完整类型
以下代码无效,但允许编译器接受:
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 &);
该标准规定,在定义模板时,不依赖模板参数的类型必须是完整的,如果它们影响程序的行为。 但是,该标准还表示编译器可以自由地不强制执行此规则。 大多数编译器在某种程度上强制执行它; 例如,在上面的代码中写入 opts.ForceReads 在 GCC 中会出错。 在 LLVM 中,一致地强制执行规则的决定提供了更好的体验,但不幸的是,它也会导致一些代码被其他编译器接受而被拒绝。
6.2.5 没有有效实例化的模板
下面的代码包含一个错字:程序员的意思是 init() 而是写了 innit() 。
template <class T> class Processor {
...
void init();
...
};
...
template <class T> void process() {
Processor<T> processor;
processor.innit(); // <-- should be ’init()’
...
}
不幸的是,编译器无法在检测到此错误后立即对其进行标记:在模板中,我们不允许对诸如 Processor 之类的“依赖类型”做出假设。 假设稍后在此文件中程序员添加了 Processor 的显式特化,如下所示:
template <> class Processor<char*> {
void innit();
};
现在程序可以运行了——但前提是程序员用 T = char* 实例化了 process()! 这就是为什么在实例化之前诊断模板定义中的错误是困难的,有时甚至是不可能的。
该标准指出,没有有效实例的模板是格式错误的。 LLVM 尝试在定义时而不是实例化时进行尽可能多的检查:这不仅会产生更清晰的诊断,而且在使用预编译头时还大大缩短了编译时间。 这种理念的缺点是 LLVM 有时无法处理文件,因为它们包含不再使用的损坏模板。 解决方法很简单:由于代码未使用,只需将其删除即可。
6.2.6 类类型const变量的默认初始化
类类型的 const 变量的默认初始化需要用户定义的默认构造函数。
如果类或结构没有用户定义的默认构造函数,C++ 不允许您默认构造它的 const 实例。 例如:
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.
...
}
6.2.7 参数名称查找
由于其实现中的错误,GCC 允许在 C++ 代码中的函数原型中重新声明函数参数名称,例如:
void f(int a, int a);
LLVM 诊断此错误(参数名称已重新声明)。 要解决此问题,请重命名其中一个参数。