(另请参阅此处了解我的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;
,当T
是类型模板参数时使用。但是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
和的任意定义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)发现名称是模板名称之后,如果此名称后跟<,则始终将<视为模板参数列表的开头,而绝不将其作为后跟-的名称。比运算符。
现在我们回到与相同的问题typename
。如果在解析代码时我们还不知道名称是否是模板怎么办?我们需要template
在模板名称之前插入,由指定14.2/4
。看起来像:
t::template f<int>(); // call a function template
模板名称不仅可以出现在a 成员之后,::
而且可以在a 成员中->
或之后出现.
。您还需要在其中插入关键字:
this->template f<int>(); // call a function template
依存关系
对于那些架子上堆满Standardese书籍,又想知道我到底在说什么的人,我会谈一谈标准中对此的规定。
在模板声明中,某些构造根据用于实例化模板的模板参数而具有不同的含义:表达式可能具有不同的类型或值,变量可能具有不同的类型,或者函数调用最终可能会调用不同的函数。通常说这样的构建体取决于模板参数。
该标准通过构造是否依赖来精确定义规则。它将它们分成逻辑上不同的组:一个捕获类型,另一个捕获表达式。表达式可能取决于它们的值和/或类型。因此,我们附有典型示例:
- 依赖类型(例如:类型模板参数
T
)
- 依赖于值的表达式(例如:非类型模板参数
N
)
- 类型相关的表达式(例如:转换为类型模板参数
(T)0
)
大多数规则是直观的,并且是递归建立的:例如,T[N]
如果N
是值依赖表达式或依赖类型,则构造为T
依赖类型的类型。有关详细信息,请参见部分((14.6.2/1
)中的相关类型,(14.6.2.2)
类型相关表达式和(14.6.2.3)
值相关表达式。
相关名称
该标准对于从属名称到底是什么还不清楚。简单阅读(您知道,最少惊奇的原理)后,它定义为从属名称的全部就是下面函数名称的特例。但是由于显然也需要在实例化上下文中进行查找,因此它也必须是从属名称(很幸运,从C ++ 14中期开始,委员会已开始研究如何解决这个令人困惑的定义)。T::x
为避免此问题,我已对标准文本进行了简单的解释。在表示依赖类型或表达式的所有构造中,它们的一个子集表示名称。因此,这些名称是“从属名称”。名称可以采用不同的形式-标准说:
名称是指表示实体或标签(6.6.4, 6.1)
标识符只是一个简单的字符/数字序列,而后两个是operator +
and operator 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声明中,不能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
};