(请参阅此处了解我的 C++11 答案)
为了解析 C++ 程序,编译器需要知道某些名称是否是类型。以下示例说明:
t * f;
这应该如何解析?对于许多语言,编译器不需要知道名称的含义就可以解析并且基本上知道一行代码的作用。然而,在 C++ 中,上述内容可能会产生截然不同的解释,具体取决于t
含义。如果它是一个类型,那么它将是一个指针的声明f
。但是,如果它不是一个类型,它将是一个乘法。所以 C++ 标准在第 (3/7) 段中说:
一些名称表示类型或模板。通常,无论何时遇到一个名称,在继续解析包含它的程序之前,都必须确定该名称是否表示这些实体之一。确定这一点的过程称为名称查找。
t::x
如果t
引用模板类型参数,编译器将如何找出名称所指的内容?x
可以是可以相乘的静态 int 数据成员,也可以同样可以是可以屈服于声明的嵌套类或 typedef。如果名称具有此属性——在知道实际模板参数之前无法查找它——那么它被称为依赖名称(它“依赖于”模板参数)。
您可能会建议等到用户实例化模板:
让我们等到用户实例化模板,然后再找出真正的含义t::x * f;
。
这将起作用,并且实际上被标准允许作为一种可能的实施方法。这些编译器基本上将模板的文本复制到内部缓冲区中,并且只有在需要实例化时,它们才会解析模板并可能检测定义中的错误。但是,与其用模板作者的错误来打扰模板的用户(可怜的同事!),其他实现选择在实例化之前尽早检查模板并在定义中尽快给出错误。
所以必须有一种方法告诉编译器某些名称是类型,而某些名称不是。
“类型名”关键字
答案是:我们决定编译器应该如何解析它。如果t::x
是一个依赖名称,那么我们需要在它前面加上前缀,typename
告诉编译器以某种方式解析它。该标准在 (14.6/2) 处说:
在模板声明或定义中使用并且依赖于模板参数的名称被假定为不命名类型,除非适用的名称查找找到类型名称或该名称由关键字 typename 限定。
有许多名称typename
是不必要的,因为编译器可以通过模板定义中适用的名称查找来确定如何解析构造本身 - 例如使用T *f;
, whenT
是类型模板参数。但是t::x * f;
要成为一个声明,它必须写成typename t::x *f;
. 如果省略关键字并且名称被视为非类型,但当实例化发现它表示类型时,编译器会发出通常的错误消息。有时,错误因此在定义时给出:
// t::x is taken as non-type, but as an expression the following misses an
// operator between the two names or a semicolon separating them.
t::x f;
该语法typename
只允许在限定名称之前- 因此,如果它们这样做,则始终知道非限定名称引用类型是理所当然的。
正如介绍性文本所暗示的,对于表示模板的名称也存在类似的问题。
“模板”关键字
还记得上面的初始报价以及标准如何要求对模板进行特殊处理吗?让我们看下面这个看似无辜的例子:
boost::function< int() > f;
对于人类读者来说,这可能看起来很明显。对于编译器来说不是这样。boost::function
想象一下and的以下任意定义f
:
namespace boost { int function = 0; }
int main() {
int f = 0;
boost::function< int() > f;
}
这实际上是一个有效的表达!它使用小于运算符boost::function
与零 ( int()
) 进行比较,然后使用大于运算符将结果bool
与进行比较f
。但是您可能很清楚,boost::function
在现实生活中是一个模板,所以编译器知道 (14.2/3):
在名称查找 (3.4) 发现一个名称是一个模板名称后,如果这个名称后跟一个 <,则 < 总是被视为模板参数列表的开头,而不是一个名称后跟 less-比运营商。
现在我们又回到了与 相同的问题typename
。如果我们在解析代码时还不知道名称是否是模板怎么办?我们需要template
在模板名称之前插入,如14.2/4
. 这看起来像:
t::template f<int>(); // call a function template
模板名称不仅可以出现在 a 之后::
,还可以出现在 a 之后->
或.
在类成员访问中。您还需要在此处插入关键字:
this->template f<int>(); // call a function template
依赖项
对于那些在书架上有厚厚的标准书并且想知道我到底在说什么的人,我会谈谈标准中是如何规定的。
在模板声明中,某些构造具有不同的含义,具体取决于您用于实例化模板的模板参数:表达式可能具有不同的类型或值,变量可能具有不同的类型,或者函数调用可能最终调用不同的函数。这种结构通常被认为依赖于模板参数。
该标准通过构造是否依赖来精确定义规则。它将它们分成逻辑上不同的组:一个捕获类型,另一个捕获表达式。表达式可能取决于它们的值和/或它们的类型。所以我们有,附加了典型的例子:
- 依赖类型(例如:类型模板参数
T
)
- 与值相关的表达式(例如:非类型模板参数
N
)
- 类型相关的表达式(例如:转换为类型模板参数
(T)0
)
大多数规则都是直观的,并且是递归构建的:例如,构造为T[N]
依赖类型的类型如果N
是值依赖表达式或T
依赖类型。(14.6.2/1
关于依赖类型、(14.6.2.2)
类型依赖表达式和(14.6.2.3)
值依赖表达式的详细信息可以在 ) 部分中阅读。
从属名称
该标准有点不清楚究竟什么是从属名称。在简单的阅读中(你知道,最不意外的原则),它定义为依赖名称的所有内容都是下面函数名称的特例。但是由于显然T::x
还需要在实例化上下文中查找,它也需要是一个依赖名称(幸运的是,从 C++14 中期开始,委员会已经开始研究如何修复这个令人困惑的定义)。
为了避免这个问题,我对标准文本进行了简单的解释。在所有表示依赖类型或表达式的结构中,它们的一个子集表示名称。因此,这些名称是“从属名称”。一个名字可以有不同的形式——标准说:
名称是标识符 (2.11)、operator-function-id (13.5)、conversion-function-id (12.3.2) 或 template-id (14.2) 的使用,表示实体或标签 (6.6.4, 6.1)
标识符只是一个简单的字符/数字序列,而接下来的两个是operator +
andoperator type
形式。最后一种形式是template-name <argument list>
。所有这些都是名称,并且按照标准中的传统用法,名称还可以包含限定符,说明应该在哪个名称空间或类中查找名称。
值相关表达式1 + N
不是名称,而是名称N
。作为名称的所有依赖结构的子集称为依赖名称。然而,函数名称在模板的不同实例中可能具有不同的含义,但不幸的是,它并没有被这个一般规则所捕获。
依赖函数名
不是本文主要关注的问题,但仍然值得一提:函数名称是一个单独处理的异常。标识符函数名称不依赖于它本身,而是依赖于调用中使用的类型依赖的参数表达式。在示例f((T)0)
中,f
是从属名称。在标准中,这在(14.6.2/1)
.
附加说明和示例
在足够多的情况下,我们需要typename
和template
。您的代码应如下所示
template <typename T, typename Tail>
struct UnionNode : public Tail {
// ...
template<typename U> struct inUnion {
typedef typename Tail::template inUnion<U> dummy;
};
// ...
};
关键字template
不必总是出现在名称的最后部分。它可以出现在用作作用域的类名之前的中间,如下例所示
typename t::template iterator<int>::value_type v;
在某些情况下,关键字是被禁止的,详情如下
在依赖基类的名字上你是不允许写的typename
。假定给定的名称是类类型名称。对于基类列表和构造函数初始化列表中的名称都是如此:
template <typename T>
struct derive_from_Has_type : /* typename */ SomeBase<T>::type
{ };
在 using-declarations 中,不能template
在 last 之后使用::
,并且 C++ 委员会表示不要制定解决方案。
template <typename T>
struct derive_from_Has_type : SomeBase<T> {
using SomeBase<T>::template type; // error
using typename SomeBase<T>::type; // typename *is* allowed
};