为什么在模板评估的第一阶段确定名称的类型,即使对于从属名称也是如此

Why is the type of a name determined during the first phase of template evaluation, even for dependent names?

本文关键字:评估 类型 定名称 段确 为什么      更新时间:2023-10-16

作为我之前提出的问题的推论,我很好奇为什么模板中名称的类型(该名称的"类别")的类型是在 2 阶段查找的第一阶段设置的,而类别本身也可以取决于模板参数。这种行为的真正收益是什么?

稍微澄清一下 - 我想我对 2 阶段查找的工作原理有很好的理解;我试图理解的是为什么在阶段 1 中明确确定令牌类别,这与确定依赖类型时不同(在阶段 2)。我的论点是,简化困难的语法,使代码更易于编写和阅读,有一个非常现实的收获,所以我很好奇将类别评估限制在阶段 1 的令人信服的理由是什么。仅仅是为了在模板实例化之前获得更好的模板验证/错误消息,还是速度略有提高?还是模板的某些基本属性使第 2 阶段类别评估不可行?

问题可能是双重的:为什么我们首先要两相查找,并且鉴于我们有两相查找,为什么在第一阶段对令牌的解释是固定的。第一个是更难回答的问题,因为它是语言中的设计决策,因此它有其优点和缺点,并且根据你的立场,那些或其他的将具有更大的权重。

第二部分,也就是你感兴趣的,实际上要简单得多。为什么,在具有两阶段查找的C++语言中,令牌含义在第一阶段是固定的,不能在第二阶段进行解释。原因是C++具有上下文语法,并且令牌的解释高度依赖于上下文。如果在第一阶段不固定令牌的含义,您甚至不知道首先需要查找哪些名称。

考虑原始代码的略微修改版本,其中文字 5 被常量表达式替换,并假设您不需要提供上次咬您的templatetypename关键字:

const int b = 5;
template<typename T>
struct Derived : public Base<T> {
void Foo() { 
Base<T>::Bar<false>(b);   // [1]
std::cout << b;           // [2]
}
};

[1]的可能含义是什么(忽略了C++这是通过添加typenametemplate来确定的事实)?

  1. Bar是一个静态模板函数,它将单个bool作为模板参数,将整数作为参数。b是一个非依赖名称,指的是常量 5。*

  2. Bar是一种嵌套模板类型,它将单个bool作为模板参数。b是在函数Derived<T>::Foo中定义且未使用的该类型的实例。

  3. Bar是类型X的静态成员变量,其比较operator<采用bool并生成一个U类型的对象,该对象可以与整数operator>进行比较。

现在的问题是我们如何在替换模板参数之前(即在第一阶段)继续解析名称。如果我们在案例 1.或 3.然后需要查找b,并且可以在表达式中替换结果。在第一种情况下产生原始代码:Base<T>::template Bar<false>(5),在后一种情况下产生operator>( operator<( Base<T>::Bar,false ), 5 )。在第三种情况下(2.),第一阶段之后的代码将与原始代码完全相同:Base<T>::Bar<false> b;(删除额外的())。

第二行[2]的含义取决于我们如何解释第一行[1]。在2.case表示对operator<<( std::cout, Base<T>::Bar<false> & )的调用,而在其他两种情况下,它表示operator<<( std::cout, 5 )。同样,含义超出了第二个参数的类型,如 2 所示。案例名称bDerived<T>::Foo是相关的,因此无法在第一阶段解析,而是推迟到第二阶段(它还将通过将Base的命名空间和实例化类型T添加到参数相关查找来影响查找)。

如示例所示,令牌的解释会影响名称的含义,进而影响代码其余部分的含义,哪些名称是依赖的或不依赖的,因此在第一阶段需要查找或不查找的其他内容。同时,编译器确实在第一次传递期间执行检查,如果令牌可以在第二次传递期间重新解释,那么第一次传递期间的检查和查找结果将变得无用(想象一下,在第一次传递期间,b被替换为5只是为了发现我们处于情况 2. 在第二阶段! 在第二阶段,一切都必须检查。

两阶段查找的存在取决于所解释的令牌及其在第一阶段选择的含义。另一种方法是像 VS 那样进行单次通过查找。


*我在这里简化了 Visual Studio 编译器中不实现两阶段查找的情况,b也可能是当前实例化类型TBase<T>成员(即它可以是依赖名称)

C++ 的许多优点是它是一种经过严格检查的语言。 您尽可能清楚地表达程序的意图,编译器会告诉您是否违反了该意图。

我无法想象你会写Base<T>::Bar<false>(b);(从Dribeas的例子中)并且没有你想要的特定解释。 通过将解释告诉编译器(Base<T>::typename Bar<false>(b);),如果有人提供具有static成员Bar或嵌套模板类型而不是成员函数模板的类型,则可能会生成有意义的错误。

其他语言被设计为强调简洁而不是静态分析;例如,许多动态语言有大量的"做我的意思"规则。 当编译器将不合理的代码转换为不可预测的、没有错误的东西时,这会引起乐趣。 (举个例子:Perl。 我喜欢它的文本操作,但天哪,DWIM 很烦人。 几乎所有内容都是运行时错误,几乎没有任何静态检查可言)