令人头疼的双阶段名称查找
C++ 拥有许多阴暗的角落,尤其是在模板方面。其中最令人头疼的一个就是“双阶段名称查找”,它涉及到对模板主体中出现的任何名称进行查找。正如你所料,模板有两个不同的阶段:- 模板定义阶段:当模板最初被解析时,也就是它被实例化之前很久,编译器会解析模板并查找任何“非依赖”名称。如果名称查找的结果不依赖于任何模板参数,那么这个名称就是“非依赖”的,并且在从一个模板实例化到另一个模板实例化时将保持一致。
- 模板实例化阶段:当模板被实例化时,编译器会查找任何“依赖”名称,此时它已经拥有了完整的模板参数集来执行查找。这次查找的结果可能会(而且经常会)因模板实例化的不同而有所不同。
双阶段名称查找并没有它的名声那样复杂。在确定什么是依赖名称与非依赖名称方面有一些不太明显的规则,但总体而言这个概念很简单。双阶段名称查找的问题在于,当前编译器对该功能的支持非常糟糕。例如,GCC 对双阶段名称查找的实现相对较好,但偶尔会延迟在模板定义阶段(阶段 1)本应该完成的查找,直到模板实例化阶段才进行,或者在不应该进行的情况下同时进行两个阶段的查找。另一方面,Visual C++ 的模板解析模型几乎会延迟所有查找,直到实例化阶段(阶段 2)才进行。由于没有完全实现双阶段名称查找,这两个编译器往往都会接受不正确的模板代码,在某些情况下,它们编译代码的方式也会与传说中的完全符合标准的编译器不同。这是一个可移植性问题,既存在于这两个编译器之间(Visual C++ 更加宽松),也存在于其他更严格的编译器。
就像 Clang 一样。Clang 的设计目标是完全支持双阶段名称查找,尽可能完整地解析模板定义(阶段 1),并且只在需要的时候在模板实例化阶段(阶段 2)进行名称查找。由于我们选择让 Clang C++ 变得严格,因此我们最终会诊断出其他编译器所忽略的模板问题。虽然这总体上是件好事——正确的代码更容易移植——但这同时也意味着 Clang 需要更加努力地生成良好的诊断信息。以下是一个 Clang 最近在 LLVM 代码库中发现的问题(该代码库用 GCC 编译):
在以下文件包含自 llvm/lib/Analysis/AliasAnalysisCounter.cpp:16在以下文件包含自 llvm/include/llvm/Pass.h:369在以下文件包含自 llvm/include/llvm/PassAnalysisSupport.h:24llvm/include/llvm/ADT/SmallVector.h:317:7: error: 使用未声明的标识符 'setEnd'setEnd(this->end()+1);^this->在以下文件包含自 llvm/lib/Analysis/AliasAnalysisCounter.cpp:16在以下文件包含自 llvm/include/llvm/Pass.h:369llvm/include/llvm/PassAnalysisSupport.h:56:14: note: 在成员函数 'llvm::SmallVectorImpl llvm::PassInfo const *>::push_back' 的实例化中请求在此处Required.push_back(ID);^在以下文件包含自 llvm/lib/Analysis/AliasAnalysisCounter.cpp:16在以下文件包含自 llvm/include/llvm/Pass.h:369在以下文件包含自 llvm/include/llvm/PassAnalysisSupport.h:24llvm/include/llvm/ADT/SmallVector.h:105:8: note必须限定标识符以在依赖的基类中找到此声明void setEnd(T *P) { this->EndX = P; }^
问题本身在于 SmallVectorImpl 中对 setEnd() 的调用。实际的 setEnd() 函数并不在 SmallVectorImpl 中,而是在一个基类中,因此我们遇到了类似于以下情况的情况:
template<typename T>class SmallVectorTemplateCommon {protectedvoid setEnd(T *P);};template<typename T>class SmallVectorImpl : public SmallVectorTemplateCommon<T> {publicvoid push_back(const T& value) {// ...setEnd(this->end() + 1);}};
如果我们不在模板中,这段代码会没问题,因为我们可以在基类中找到 setEnd。但是,由于我们是在模板中,因此我们正在处理双阶段名称查找。在解析 push_back() 时,编译器会在阶段 1 中对名称“setEnd”进行名称查找:但是,它什么也找不到,因为它不允许查看依赖的基类 SmallVectorTemplateCommon<T>。但是,这段代码仍然有效:“setEnd”被视为非成员函数的名称,它可以通过依赖参数查找在实例化时找到。不幸的是,当我们最终实例化 push_back 时,依赖参数查找不会查看我们的基类,因此 Clang 会给我们一个“使用未声明的标识符”错误。
就它本身而言,这个错误会导致程序员摸不着头脑。GCC 和 Visual C++ 接受了这段代码,而且 setEnd() 显然在基类中,那么问题出在哪里?为了提供一些帮助,Clang 提供了更详细的信息:
- 最后面的注释中写着“必须限定标识符以在依赖的基类中找到此声明”,它告诉程序员 Clang 可以找到哪个声明……如果她能以某种方式限定这个名称,从而允许 Clang 在那里查找。
- 原始错误在插入符诊断的下方有一个绿色的提示,提供了解决此特定问题的建议。通过添加“this->”,我们告诉编译器“setEnd”位于当前类或其(可能是依赖的)基类中,并在模板实例化阶段查找。
Clang C++ 的设计目标是成为一个严格但有帮助的编译器,它遵循 C++ 标准的字面意思,帮助程序员确保他们的代码具有可移植性。我们也希望让 Clang 成为一个友好的编译器,它可以利用它对程序和 C++ 语言的了解,帮助程序员克服像这样可移植性问题。或许,Clang 可以照亮 C++ 中阴暗、可怕的角落。