这篇文章的作者是Tanveer Gani,Stephan T。拉瓦维、安德鲁·马里诺、加布里埃尔·多斯雷斯和安德鲁·帕多
“两阶段名称查找”是一个非正式术语,指的是一组规则,用于管理模板声明中使用的名称的解析。 这些规则是正式的 二十多年前,人们试图调和两种对立的模板编译模型:包含模型(今天大多数开发人员都知道的模板)和分离模型(模板原始设计的基础)。你可以在基础论文中找到从属名称的起源 模板规范的拟议修订 ,坚定地立足于一个定义的原则。如果你有兴趣深入了解这些精彩的细节,你可以在第17.6节中找到这些现代的规则 (系统的稳定名称[temp.res]) C++ 17标准草案 . 在过去的几个月里,MSVC编译器已经从不支持两阶段名称查找变成可以在大多数代码上使用。我们将在未来的Visual Studio 2017更新中完成对此功能的完全支持。
你需要使用 /permissive-
一致性开关 在Visual Studio 2017“15.3”附带的MSVC编译器中启用两阶段查找。两阶段名称查找会彻底更改某些代码的含义,因此在当前版本的MSVC中,默认情况下不会启用该功能。
这篇文章分析了两阶段名称查找到底需要什么,MSVC目前实现了什么,以及如何有效地利用MSVC对两阶段名称查找的部分但实质性的支持。我们还将告诉您如何选择退出两阶段查找,即使您希望代码的其余部分严格符合标准。最后,我们将解释一下为什么我们花了这么长时间才来到这里这些规则至少有25年的历史了!
什么是“两阶段名称查找”?
原来的C++模板设计意在精确地描述“模板”这个词的含义:模板将剔除类和函数的类。它允许并鼓励(但不要求)尽早检查非从属姓名。因此,在解析模板定义时不需要查找标识符。相反,编译器被允许延迟名称查找,直到模板被实例化。类似地,在实例化之前不需要验证模板的语法。从本质上讲,模板中使用的名称的含义直到模板被实例化后才被确定。
根据这些原始规则,以前版本的MSVC做了非常有限的模板解析。 特别是,函数模板体在实例化之前根本没有被解析。编译器将模板体记录为一个令牌流,在模板的实例化过程中,当需要时,该令牌流会被重放,而模板可能是候选的。
让我们通过查看一段代码来考虑这意味着什么。提供了到联机编译器的链接,以便您可以在阅读本文时使用这些代码。
#include <cstdio>void func(void*) { std::puts("The call resolves to void*") ;}template<typename T> void g(T x){ func(0);}void func(int) { std::puts("The call resolves to int"); }int main() { g(3.14);}
第7行的调用解决了哪些重载?这个 void*
在第5行编写模板时已声明重载。函数 void func(int)
在编写模板时不存在。因此,在第14行调用函数模板 void g(T x)
在第5行应该解决的功能 void func(void*)
在第三行。
使用符合标准的编译器编译时,此程序将打印“ The call resolves to void*
“. 你可以 在GCC中查看此行为 使用Rextester联机编译器。 使用Visual Studio 2015中的MSVC ,不支持两阶段名称查找,程序打印“调用解析为int”。
为什么MSVC搞错了?当模板很简单时,我们用来解析模板的机制就起作用了,但是当两阶段名称查找开始起作用时,编译器所能做的就有限了。MSVC以前将模板体记录为令牌流,并将该流存储起来,以便在实例化时重放。MSVC从记录的令牌流中替换模板的行为与宏替换的行为有些相似,因为对模板的主体进行了有限的分析。
在本例中,MSVC为函数模板存储了一个令牌流 void g(T x)
. 如果编译器在遇到函数调用时分析了函数调用,则只有 void func(void*)
会在超负荷组(请注意,这是该调用的有效匹配项 func(0)
因为C++允许 0
代表 可以转换为任何指针类型的空指针常量 .)
函数重载 void func(int)
也会是一个匹配的电话 func(0)
只是它不应该在函数模板的重载集中 void g(T x)
进行了评估。但是MSVC直到声明之后的实例化点才计算模板的主体 void func(int)
已添加到过载集。此时,编译器为整数参数选择了更好的匹配项: int
而不是 void*
.
您可以在中看到两个编译器都在运行 此代码示例位于联机编译器资源管理器上 . 当注释掉第3行时,GCC拒绝编译代码示例,而MSVC很高兴地匹配一个在编写模板时甚至没有定义的函数。如果它不是一个模板,它将被认为是非法代码,但是我们的模板替换机制允许编译器接受这段代码。
C++标准委员会意识到,在模板中编写的代码不应受到周围环境的微妙影响,同时也要维护ODR。他们 引入了 依赖的 和 非独立 姓名 在模板的名称绑定规则中,因为在第10行编写函数会改变上面代码的含义,这是一种令人惊讶的行为。
标准的[temp.res]部分中的规则列出了三种名称:
- 模板的名称和在模板中声明的名称
- 依赖于模板参数的名称
- 模板定义中可见范围中的名称
第一类和第三类是非从属名称。它们在模板的定义点处绑定,并在该模板的每个实例化中保持绑定。当模板被实例化时,它们永远不会被查找(看到了吗§17.6[温度分辨率]/10和§17.6.3[温度非深度] 标准草案 有关详细信息。)
第二类是从属名称。依赖名称不绑定在模板的定义点。相反,在实例化模板时会查找这些名称。对于具有从属函数名称的函数调用,该名称将绑定到模板定义中调用点处可见的函数集。在模板定义点和模板实例化点都添加了来自参数相关查找的其他重载(看到了吗§17.6.2[温度下降],§17.6.4【临时折旧】和§17.6.4.2【临时部门候选人】 标准草案 有关详细信息。)
需要注意的是,在模板定义点之后但在模板实例化点之前声明的重载只有在通过参数相关的查找找到时才被考虑。MSVC以前没有将参数相关的查找与普通的、非限定的查找分开进行,因此这种行为上的变化可能令人惊讶。
考虑这个代码示例,它也是 可在Wandbox在线编译器上获得 :
#include <cstdio> void func(long) { std::puts("func(long)"); }template <typename T> void meow(T t) { func(t);}void func(int) { std::puts("func(int)"); }namespace Kitty { struct Peppermint {}; void func(Peppermint) { std::puts("Kitty::func(Kitty::Peppermint)"); }}int main() { meow(1729); Kitty::Peppermint pepper; meow(pepper);}
电话 meow(1729)
决心 void func(long)
超载,不是吗 void func(int)
超载,因为不合格 func(int)
在模板的定义之后声明,而不是通过参数相关的查找找到。但是 void func(Peppermint)
不参与参数相关的查找,因此它被添加到调用的重载集中 meow(pepper)
.
从上面的例子中,您可以看到“两阶段查找”的两个阶段是在模板定义时查找非依赖名称和在模板实例化时查找依赖名称。
Visual Studio 2017“15.3”之前的MSVC行为
历史上,遇到模板时,MSVC编译器会执行以下步骤:
- 在解析类模板时,MSVC以前只解析模板声明、类头和基类列表。模板主体被捕获为令牌流。没有函数体、初始值设定项、默认参数或
noexcept
已分析参数。类模板是在“暂定”类型上伪实例化的,以验证类模板中的声明是否正确。以该类模板为例:template <typename T> class Derived : public Base<T> { ... }
. 模板声明,template <typename T>
,班长,class Derived
,以及基类列表,public Base<T>
但是模板体,{ ... }
,被捕获为令牌流。 - 在解析函数模板时,MSVC以前只解析函数签名。函数体从未被解析,它被捕获为令牌流。因此,如果模板体有语法错误,并且模板从未实例化,则不会诊断错误。
这个行为如何导致不正确解析的例子可以从MSVC如何不需要关键字中看出 template
和 typename
无论它们在哪里,C++标准都需要它们。在某些位置需要这些关键字来消除编译器在查找的第一阶段如何解析依赖名称的歧义。例如,考虑以下代码行:
T::Foo<a || b>(c);
此代码是对参数为的函数模板的调用吗 a || b
? 或者这是一个逻辑或表达式 T::foo <
a作为左操作数 b > (c)
作为正确的操作数?
一致性编译器将Foo解析为T范围内的变量,这意味着该代码是两个比较之间的or操作。如果你想用 Foo
作为函数模板,必须通过添加template关键字来指示这是一个模板,例如。,
T::template Foo<a || b>(c);
在Visual Studio 2017“15.3”之前,MSVC允许此代码 没有 template关键字,因为它以非常有限的方式解析模板。上面的代码在第一阶段根本不会被解析。在第二阶段有足够的背景来说明这一点 T::Foo
是一个模板而不是一个变量,所以MSVC没有强制使用关键字。
通过删除关键字也可以看到这种行为 typename
在函数模板体、初始值设定项、默认参数和 noexcept
论据。考虑以下代码:
template<typename T>typename T::TYPE func(typename T::TYPE*){ typename T::TYPE i;}
如果删除关键字 typename
在第4行的函数体中,MSVC仍将编译此代码,而一致性编译器将拒绝此代码。你需要 typename
关键字来指示 TYPE
是依赖的。因为MSVC以前没有解析主体,所以它不需要关键字。 您可以在联机编译器资源管理器中看到这个示例 . 由于是在MSVC一致性模式下编译的( /permissive-
),将导致错误,当您前进到MSVC版本19.11及更高版本时,请确保查找 typename
缺少关键字。
类似地,在这个代码示例中:
template<typename T>typename T::template X<T>::TYPE func(typename T::TYPE){ typename T::template X<T>::TYPE i;}
MSVC以前只需要 template
关键字。一致性编译器需要 template
第4行的关键字来表示 T::X<T>
是一个模板。在中取消对关键字的注释 编译器资源管理器上的此示例 去看看行动中的错误。同样,在向前移动代码时,请记住缺少的关键字。
Visual Studio 2017“15.3”中的两阶段名称查找
我们在Visual Studio 2017中引入了一个“一致性模式”开关 /放任的- 转换 打开此一致性模式(在下一个主要的编译器版本中,一致性模式将默认启用。此时,您将能够使用/permissive开关来请求非一致模式(没有 -
)很像 -fpermissive
当我们引入 /permissive-
switch是两阶段名称查找,现在已在VS2017“15.3”附带的编译器中部分实现。
我们的两阶段名称查找支持缺少几个部分请参阅一节 “接下来是什么” 详情请参见下文。但MSVC编译器现在可以正确解析并严格执行以下语法规则:
- 类模板
- 函数模板体和类模板的成员函数
- 初始值设定项,包括成员初始值设定项
- 默认参数
-
noexcept
论据
此外,STL的MSVC实现是完全两阶段干净的(由 /permissive-
在MSVC和Clang’s -fno-ms-compatibility -fno-delayed-template-parsing
). 我们最近已经得到了ATL是两个阶段的清洁;如果您发现任何遗留的错误,请务必让我们知道!
但是,对于可能依赖旧的、不正确的MSVC行为的遗留代码,您该怎么做呢?你还可以用 /permissive-
对于其余的一致性改进,即使您的代码还没有完全准备好对模板体进行解析并正确绑定依赖名称。把枪扔出去 /Zc:twoPhase-
切换以关闭模板解析和依赖名称绑定。使用此开关将导致MSVC编译器使用具有非标准语义的旧行为,从而使您有机会修复代码,以便使用符合标准的MSVC编译器正确编译。
如果您使用的是带有 /permissive-
切换时,需要使用 /Zc:twoPhase-
切换到Windows RedStone 3(“秋季创建者更新”)SDK可用。这是因为Windows团队一直在与MSVC团队合作,通过两阶段名称查找使SDK头正常工作。在RedStone3 Windows SDK发布之前,它们的更改将不可用,两阶段名称查找的更改也不会移植回RedStone2 Windows SDK。
接下来是什么
MSVC对两阶段名称查找的支持正在进行中。以下是在Visual Studio 2017中MSVC的未来更新中剩下的内容列表。请记住,您需要使用 /permissive-
切换这些示例以启用两阶段查找。
- 不诊断模板中未声明的标识符。例如。
template<class T>void f(){ i = 1; // Missing error: `i` not declared in this scope}
MSVC不会发出
`i`
未声明,代码编译成功。添加的实例化f
导致生成正确的错误:template<class T>void f(){ i = 1; // Missing error: `i` not declared in this scope}void instantiate(){ f<int>();}
C: mp> cl /c /permissive- /diagnostics:caret one.cppMicrosoft (R) C/C++ Optimizing Compiler Version 19.11.25618 for x64Copyright (C) Microsoft Corporation. All rights reserved.one.cppc: mpone.cpp(4,5): error C2065: 'i': undeclared identifier i = 1; ^c: mpone.cpp(9): note: see reference to function template instantiation 'void f<int>(void)' being compiled f<int>();
- 带有VS 2017“15.3”的MSVC编译器将生成一个缺失的错误
template
和typename
关键字,但不建议添加这些关键字。较新的编译器版本提供了更多的信息诊断。template <class T>void f() { T::Foo<int>();}
VS 2017“15.3”附带的MSVC编译器出现以下错误:
C: mp>cl /c /permissive- /diagnostics:caret two.cppMicrosoft (R) C/C++ Optimizing Compiler Version 19.11.25506 for x64Copyright (C) Microsoft Corporation. All rights reserved.two.cpptwo.cpp(3,16): error C2187: syntax error: ')' was unexpected here T::Foo<int>(); ^
将随VS 2017未来更新提供的编译器版本给出了一个更详细的错误:
C: mp>cl /c /permissive- /diagnostics:caret two.cppMicrosoft (R) C/C++ Optimizing Compiler Version 19.11.25618 for x64Copyright (C) Microsoft Corporation. All rights reserved.two.cpptwo.cpp(3,7): error C7510: 'Foo': use of dependent template name must be prefixed with 'template' T::Foo<int>(); ^two.cpp(3,4): error C2760: syntax error: unexpected token 'identifier', expected 'id-expression' T::Foo<int>(); ^
- 编译器在参数相关查找过程中未正确查找函数。这可能导致在运行时调用错误的函数。
#include <cstdio>namespace N{ struct X {}; struct Y : X {}; void f(X&) { std::puts("X&"); }}template<typename T>void g(){ N::Y y; f(y); // This is non-dependent but it is not found during argument-dependent lookup so it is left unbound.}void f(N::Y&){ std::puts("Y&");}int main(){ g<int>();}
运行此程序的输出如下
Y&
应该什么时候X&
.C: mp>cl /permissive- /diagnostics:caret three.cppMicrosoft (R) C/C++ Optimizing Compiler Version 19.11.25506 for x64Copyright (C) Microsoft Corporation. All rights reserved.three.cppMicrosoft (R) Incremental Linker Version 14.11.25506.0Copyright (C) Microsoft Corporation. All rights reserved./out:three.exethree.objC: mp>threeY&
- 未正确分析包含局部声明的非类型依赖表达式。MSVC编译器当前将该类型解析为依赖类型,从而导致错误。
template<int> struct X { using TYPE = int; };template<typename>void f(){ constexpr int i = 0; X<i>::TYPE j;}
出现语法错误,因为
i
当第9行上表达式的值不依赖于类型时,未正确分析为非值依赖表达式。C: mp>cl /c /permissive- /diagnostics:caret four.cppMicrosoft (R) C/C++ Optimizing Compiler Version 19.11.25618 for x64Copyright (C) Microsoft Corporation. All rights reserved.four.cppfour.cpp(10,16): error C2760: syntax error: unexpected token 'identifier', expected ';' X<i>::TYPE j; ^four.cpp(10,5): error C7510: 'TYPE': use of dependent type name must be prefixed with 'typename' X<i>::TYPE j; ^
- 模板参数的重新声明和模板函数参数作为本地名称的重新定义都不会报告为错误。
template<class T>void f(int i){ double T = 0.0; // Missing error: Declaration of `T` shadows template parameter float i = 0; // Missing error: Redefinition of `i` with a different type}
- 在某些情况下,MSVC编译器错误地识别了当前的实例化。使用关键字
typename
是合法的,有助于编译器正确识别当前实例化。template<class T> struct A { typedef int TYPE; A::TYPE c1 = 0; // Incorrectly fails to compile A<T>::TYPE c2 = 0; // Incorrectly fails to compile};
添加关键字
typename
在每个实例之前A
允许此代码编译:template<class T> struct A { typedef int TYPE; typename A::TYPE c1 = 0; typename A<T>::TYPE c2 = 0;};
- 不诊断未声明的默认参数。此示例演示了MSVC编译器仍在执行单阶段查找的情况。它使用的是
SIZE
在模板声明之后找到,就像在模板之前声明一样。template<int N = SIZE> // Missing diagnostic: Use of undeclared identifier `SIZE`struct X{ int a[N];};constexpr int SIZE = 42;X<> x;
以上所有问题都计划在VisualStudio2017的MSVC下一次重大更新中修复。
为什么要花这么长时间?
其他编译器已经有相当一段时间实现了两阶段名称查找。为什么MSVC现在做对了?
实现两阶段名称查找需要对MSVC的体系结构进行根本性的更改。最大的变化是 编写一个新的递归下降解析器来替换基于YACC的解析器 我们已经用了35年了。
我们很早就决定遵循增量路径,而不是从头开始重写编译器。将过时的MSVC代码库演化为更现代的代码库,而不是在大的重写中“变暗”,这样我们就可以在编译现有代码时进行巨大的更改,而不会引入细微的错误和破坏性的更改。我们的“编译器复兴”工作需要小心地将旧代码和新代码连接起来,确保所有的时间,现有代码的大型测试套件继续编译完全相同(除非我们故意要进行更改以引入一致性行为)。以这种方式做这项工作需要更长的时间,但这允许我们为开发人员提供增量价值。我们已经能够在不意外地破坏现有代码的情况下进行重大更改。
最后
我们很高兴终于在MSVC中支持两阶段名称查找。我们知道,编译器仍然不会编译一些模板代码正确如果你发现一个案例没有提到在这篇文章,请联系我们,以便我们可以修复错误!
本文中的所有代码示例现在都按照标准正确编译(或者在适当的时候无法编译)。您将在VisualStudio2017“15.3”中看到这种新行为,或者现在就可以尝试 使用MSVC编译器的每日版本 .
现在是开始使用 /permissive-
转换 将代码向前移动。还记得在添加关键字时遇到模板解析错误吗 template
和 typename
MSVC以前不需要( 见上文 )可能会修复错误。
如果您对我们有任何反馈或建议,请告知我们。我们可以通过以下评论和电子邮件联系到您( visualcpp@microsoft.com )你可以通过 帮助>报告产品中的问题 ,或通过 开发者社区 . 你也可以在Twitter上找到我们( @视觉 )还有Facebook( msftvisualcpp软件 ).