为什么我必须通过这个指针访问模板基类成员?

如果下面的类不是模板,我可以简单地在derived类中有x 。 但是,使用下面的代码,我必须使用this->x 。 为什么?

 template <typename T> class base { protected: int x; }; template <typename T> class derived : public base<T> { public: int f() { return this->x; } }; int main() { derived<int> d; df(); return 0; } 

简短的回答:为了使x一个独立的名字,以便查询推迟到模板参数已知。

长的答案:当编译器看到一个模板时,应该立即执行某些检查,而不会看到模板参数。 其他的被推迟到参数已知。 这就是所谓的两阶段编译,MSVC并没有这样做,但这是标准所要求的,并且由其他主要的编译器来实现。 如果你愿意的话,编译器必须在看到它的时候立即编译这个模板(对于某种内部的分析树表示),然后推迟编译实例化,直到稍后。

在模板本身上执行的检查,而不是特定的实例化,需要编译器能够parsing模板中的代码的语法。

在C ++(和C)中,为了parsing代码的语法,你有时需要知道某个东西是不是一个types。 例如:

 #if WANT_POINTER typedef int A; #else int A; #endif static const int x = 2; template <typename T> void foo() { A *x = 0; } 

如果A是一个types,那么声明一个指针(除了影响全局x之外,没有任何效果)。 如果A是一个对象,这是乘法(并且禁止一些运算符重载它是非法的,分配给一个右值)。 如果错误,则必须在阶段1中诊断出这个错误,它被标准定义为模板中的错误,而不是在某个特定的实例中。 即使模板从来没有实例化,如果A是一个int那么上面的代码是foo不对的,必须被诊断,就像foo根本不是一个模板,而是一个普通的函数。

现在,这个标准表示, 依赖于模板参数的名称必须在阶段1中可以parsing。这里不是依赖名称,它指的是同一件事情,不pipetypes是什么。 所以需要在模板定义之前定义,以便在阶段1中find并检查。

T::A将取决于T的名称。我们不可能在阶段1中知道这是否是一种types。 最终在实例化中最终被用作T的types甚至还没有被定义,即使它是我们不知道哪个types将被用作我们的模板参数。 但是我们必须解决这个语法,以便对不合格的模板进行宝贵的阶段1检查。 所以标准有一个依赖名称的规则 – 编译器必须假设它们是非types的,除非用typename限定它们types的,或者在某些明确的上下文中使用。 例如在template <typename T> struct Foo : T::A {};T::A被用作基类,因此是一个明确的types。 如果Foo使用具有数据成员A而不是嵌套typesA的某种types实例化,那么在执行实例化(阶段2)的代码中出现错误,而不是模板(阶段1)中的错误。

但是具有相关基类的类模板呢?

 template <typename T> struct Foo : Bar<T> { Foo() { A *x = 0; } }; 

是不是一个独立的名字? 有了基类, 任何名字都可以出现在基类中。 所以我们可以说A是一个独立的名字,并把它当作一个非types的名字。 这会产生不希望的结果,即Foo中的每个名称都是依赖的,因此Foo中使用的每个types (除了内置types)都必须是合格的。 在Foo里面,你必须写下:

 typename std::string s = "hello, world"; 

因为std::string将是一个依赖名称,并且因此被认为是非types的,除非另外指定。 哎哟!

允许你的首选代码( return x; )的第二个问题是即使BarFoo之前定义,并且x不是该定义中的成员,稍后有人可以为某些types的Baz定义Bar的专门化,例如Bar<Baz>确实有一个数据成员x ,然后实例化Foo<Baz> 。 所以在这个实例中,你的模板将返回数据成员,而不是返回全局x 。 或者相反,如果Bar的基本模板定义有x ,那么他们可以在没有它的情况下定义一个专门化,并且你的模板将寻找一个全局x来在Foo<Baz>返回。 我认为这被认为与你所遇到的问题一样令人惊讶和痛苦,但这却是令人惊讶的,而不是抛出令人惊讶的错误。

为了避免这些问题,标准实际上表示,除非名称因为其他原因而已经依赖,否则不会search类模板的相关基类。 这阻止了一切从依赖,因为它可以在一个依赖的基地。 它也有你所看到的不良影响 – 你必须从基类资格或没有find。 有三种常见的方法来使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是基类中的嵌套types,那么它就没有意义,所以语言应该(a)说它是一个独立的名字,并且(2)把它当作一个非types的,并且你的代码在没有this-> 。 在某种程度上,你是解决scheme所带来的间接损害的受害者,而这个问题不适用于你的情况,但是你的基类可能会在你的影子名下引入名字,或者没有你想到的名字他们有,并find一个全球性的。

你也可以争辩说,默认应该是相反的名字相反(假设types,除非以某种方式被指定为一个对象),或者默认应该更多的上下文敏感(在std::string s = ""; std::string可以作为一个types读取,因为没有其他的语法意义,尽pipestd::string *s = 0;是不明确的)。 再次,我不太清楚规则是如何得到同意的。 我的猜测是,所需的文本页数,缓解创造了很多具体的规则,上下文采取一种types和哪一种非types。

(2011年1月10日原创)

我想我已经find了答案: GCC问题:使用依赖于模板参数的基类的成员 。 答案不是特定于gcc。


更新:为了回应mmichael对C ++ 11标准草案 N3337的评论 :

14.6.2相关名称[temp.dep]
[…]
3在类或类模板的定义中,如果基类依赖于模板参数,则在类模板或成员的定义点或者在实例化类模板或成员。

无论是“因为标准如此”都算作答案,我不知道。 现在我们可以问为什么这个标准要求,但是作为史蒂夫·杰索普的杰出回答和其他人指出的,后面这个问题的答案是相当长的和可论证的。 不幸的是,当涉及到C ++标准时,为了标准要求某些东西,通常几乎不可能给出一个简短而独立的解释。 这也适用于后一个问题。

x在inheritance期间被隐藏。 您可以通过以下方式取消隐藏:

 template <typename T> class derived : public base<T> { public: using base<T>::x; // added "using" statement int f() { return x; } };