简短的回答:为了创建 x
一个依赖名称,以便查找被推迟到知道模板参数为止。
长答案:当编译器看到模板时,它应该立即执行某些检查,而无需查看模板参数。其他检查则推迟到参数已知时执行。这称为两阶段编译,MSVC 不这样做,但这是标准所要求的,并由其他主要编译器实现。如果您愿意,编译器必须在看到模板后立即编译模板(编译为某种内部解析树表示),并将实例化的编译推迟到稍后。
对模板本身而不是其特定实例执行的检查要求编译器能够解析模板中代码的语法。
在 C++(和 C)中,为了解析代码的语法,有时需要知道某个东西是否是类型。例如:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
如果 A 是类型,则声明一个指针(除了遮蔽全局变量之外没有其他效果 x
)。如果 A 是对象,则执行乘法(除非某些运算符重载,否则赋值给右值是非法的)。如果是错误的,则必须 在阶段 1 中 ,标准将其定义为 模板中的 ,而不是模板的某些特定实例中的错误。即使模板从未被实例化,如果 A 是, int
则上述代码格式不正确,必须进行诊断,就像它 foo
根本不是模板而是普通函数一样。
现在,标准规定 不 依赖于模板参数的名称必须在阶段 1 中可解析。 A
这里不是依赖名称,无论类型如何,它都指的是同一个东西 T
。因此,需要在定义模板之前定义它,以便在阶段 1 中找到和检查它。
T::A
将是一个依赖于 T 的名称。我们不可能在第 1 阶段知道这是否是一种类型。最终将 T
在实例化中用作的类型很可能尚未定义,即使定义了,我们也不知道将使用哪种类型作为模板参数。但我们必须解决语法问题,以便执行宝贵的第 1 阶段检查以查找格式错误的模板。因此,标准对依赖名称有一条规则 - 编译器必须假定它们是非类型,除非使用 限定来 typename
指定它们 是 类型,或在某些明确的上下文中使用。例如,在 中 template <typename T> struct Foo : T::A {};
, T::A
用作基类,因此显然是一种类型。如果 Foo
用具有数据成员的某种类型而不是嵌套类型 A 来实例化 A
,那么这是执行实例化(第 2 阶段)的代码中的错误,而不是模板(第 1 阶段)中的错误。
但是,如果类模板具有依赖基类,情况会怎样呢?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
A 是否是依赖名称?对于基类, 任何 名称都可以出现在基类中。因此,我们可以说 A 是一个依赖名称,并将其视为非类型。这会产生不良影响,即 每个名称 都是依赖的,因此 每种类型 (内置类型除外)都必须经过限定。在 Foo 内部,您必须编写:
typename std::string s = "hello, world";
因为 std::string
将是一个从属名称,因此除非另有说明,否则将假定为非类型。哎哟!
允许您首选代码 ( return x;
) 的第二个问题是,即使 Bar
之前已定义 Foo
,并且 x
不是该定义中的成员,有人以后可能会 Bar
为某些类型 定义 Baz
,例如 Bar<Baz>
确实具有数据成员 x
,然后实例化 Foo<Baz>
。因此,在该实例中,您的模板将返回数据成员而不是返回全局 x
。或者相反,如果 的基本模板定义 Bar
有 x
,他们可以定义一个没有它的特化,并且您的模板将在 中寻找要 x
返回的 Foo<Baz>
。我认为这被认为与您的问题一样令人惊讶和沮丧,但它是 默默地 令人惊讶,而不是抛出一个令人惊讶的错误。
为了避免这些问题,标准实际上规定,除非明确要求,否则不会考虑搜索类模板的依赖基类。这样一来,所有内容就不会因为可以在依赖基类中找到而成为依赖项。它还会产生您所看到的不良影响 - 您必须限定基类中的内容,否则找不到它。有三种常见的方法可以进行 A
依赖:
-
using Bar<T>::A;
在类中 - A
现在指的是中的某物 Bar<T>
,因此是依赖的。
-
Bar<T>::A *x = 0;
在使用时 - 再次强调, A
肯定在 Bar<T>
。这是乘法,因为 typename
没有使用,所以可能是一个不好的例子,但我们必须等到实例化才能知道是否 operator*(Bar<T>::A, x)
返回右值。谁知道呢,也许确实如此……
-
this->A;
在使用时 - A
是一个成员,所以如果它不在中 Foo
,它必须在基类中,标准再次表明这使它具有依赖性。
两阶段编译既繁琐又困难,并且会引入一些令人惊讶的额外代码要求。但就像民主一样,除了所有其他方式外,它可能是最糟糕的做事方式。
您可以合理地争辩说,在您的示例中, return x;
是基类中的嵌套类型,则 x
没有意义 this->
。在某种程度上,您是解决不适用于您的情况的问题的附带损害的受害者,但仍然存在这样的问题:您的基类可能会在您之下引入影响全局变量的名称,或者没有您认为它们有的名称,而是找到了全局变量。
您可能还会认为,默认设置应该与从属名称相反(除非以某种方式指定为对象,否则假设类型),或者默认设置应该更敏感于上下文(in std::string s = "";
, std::string
可以读作类型,因为其他任何东西都没有语法意义,尽管 std::string *s = 0;
是模棱两可的)。同样,我不知道规则是如何达成一致的。我的猜测是,所需的文本页数可以减轻创建大量特定规则的负担,这些规则规定哪些上下文采用类型,哪些上下文采用非类型。