C语言中Two Phase Lookup模板代码为何常引发编译错误?
摘要:猜猜下面这段代码的输出是什么: template <typename T> struct Base { void DoThings() { std::cout <&a
猜猜下面这段代码的输出是什么:
template <typename T>
struct Base {
void DoThings() {
std::cout << "A\n";
}
};
template <typename T>
struct Derived: Base<T> {
void Do() {
DoThings();
}
};
int main() {
Derived<int> d;
d.Do();
}
肯定有人会说是A,但实际上是编译错误:
test.cpp: In member function 'void Derived<T>::Do()':
test.cpp:9:17: error: there are no arguments to 'DoThings' that depend on a template parameter, so a declaration of 'DoThings' must be available [-Wtemplate-body]
9 | DoThings();
| ^~~~~~~~
test.cpp:9:17: note: (if you use '-fpermissive', G++ will accept your code, but allowing the use of an undeclared name is deprecated)
给的报错信息很让人迷惑,因为DoThings是明确声明定义在Base<T>中的,这里居然在说它未被声明。
这其实是c++的Two Phase Lookup导致的。
Two Phase Lookup如其字面意思,对于任何模板代码,编译器需要进行两次检查:
Phase 1,第一步检查,只检查模板代码是否有语法错误,但涉及到和模板类型参数相关的部分会跳过。检查的范围包括是否有明显的语法错误比如用了不存在的关键字、少了分号等,其中也会检查那些和模板类型参数无关的函数、类型、方法是否已经被声明,这和编译器检查普通代码的流程很相似
Phase 2,这一步会往模板的参数里带入实际的类型,编译器会重新推导整个模板代码在当前的类型下是否合法
两步骤是为了更快速地将类型参数不相关的问题排除,这样在保证模板代码语法正确性的同时尽量保证了泛型代码的灵活性,理想中也能让模板的编写者更快发现问题而不是把问题延迟到类型推导之后。
但坏处就是会让模板产生一下诡异的编译错误了,比如上面的DoThings。DoThings在这里是非限定名称,但没有参数,同时它也和Derived模板的类型参数不直接相关,这导致对DoThings的检查会在Phase 1执行,而Phase 1会忽略所有的模板参数相关内容,这导致Base<T>在这时不可见,而我们又没有在其他地方定义DoThings,所以编译器认为我们在使用一个未声明的符号,于是报了语法错误。
