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

如果下面的类不是模板,我可以简单地在derived类中有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;
d.f();
return 0;
}
29694 次浏览

x在继承过程中被隐藏。你可以通过以下方式取消隐藏:

template <typename T>
class derived : public base<T> {


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

(2011年1月10日的原始答案)

我想我已经找到答案:GCC问题:使用依赖于模板参数的基类成员。 答案并不特定于gcc。< / p >


更新:作为对mmichael的评论的响应,来自c++ 11 Standard的N3337草案:

< p > 14.6.2依赖名称[temp.dep] < br > […]< br > 在类或类模板的定义中,如果基类依赖于 模板参数时,基类作用域在非限定名称期间不检查 在类模板的定义点进行查找 或类模板或成员的实例化过程中。< / p >

"因为标准是这么说的"是否算作答案,我不知道。我们现在可以问为什么标准要求这样做,但正如史蒂夫·杰索普的精彩回答和其他人指出的那样,后一个问题的答案相当长且有争议。不幸的是,当涉及到c++标准时,几乎不可能给出一个简短的、自成体系的解释来解释为什么标准要求某些东西;这同样适用于后一个问题。

简单回答:为了使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是一个对象,这是乘法运算(并且禁止一些操作符重载,它是非法的,赋值给右值)。如果它是错误的,这个错误必须被诊断为在第一阶段,它被标准定义为错误在模板中,而不是在它的某些特定实例化中。即使模板从未实例化,如果A是int,则上述代码是格式错误的,必须进行诊断,就像如果foo根本不是模板,而是一个普通函数一样。

现在,标准规定依赖于模板参数的名称必须在阶段1中可解析。A在这里不是一个依赖名称,它指的是相同的东西,无论类型是T。因此,需要在定义模板之前定义它,以便在阶段1中找到和检查。

T::A将是一个依赖于t的名称。在阶段1中,我们不可能知道它是否是一个类型。最终在实例化中作为T使用的类型很可能还没有定义,即使定义了,我们也不知道哪个类型将被用作模板形参。但是我们必须解决语法问题,以便进行宝贵的第一阶段检查,检查格式不正确的模板。因此标准对依赖名称有一个规则——编译器必须假设它们是非类型的,除非用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中的每一个名字是依赖的,因此Foo中使用的每一种类型(内置类型除外)必须是限定的。在Foo内部,你必须这样写:

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

因为std::string将是一个依赖名称,因此假设为非类型,除非另有指定。哎哟!

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

为了避免这些问题,该标准实际上规定除非明确请求,否则类模板的依赖基类不会被考虑用于搜索。这使得所有东西都不再是依赖的,因为它可以在依赖基中找到。它也有您所看到的不良影响-您必须从基类中限定内容,否则就找不到它。有三种常见的方法使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中,它必须在基类中,同样,标准说这使它具有依赖性。

两阶段编译既精细又困难,并且在代码中引入了一些令人惊讶的额外冗余要求。但就像民主一样,它可能是最糟糕的做事方式。

你可以合理地争辩说,在你的例子中,如果x是基类中的嵌套类型,return x;就没有意义了,所以语言应该(a)说它是一个依赖名称,(2)将它视为非类型,并且你的代码没有this->也能工作。在某种程度上,您是问题解决方案的附带损害的受害者,该解决方案并不适用于您的情况,但仍然存在基类可能在您下面引入的名称与全局变量相关联的问题,或者没有您认为它们具有的名称,而是找到了全局变量。

你也可以争辩说,对于依赖名称,默认值应该是相反的(假设类型,除非以某种方式指定为对象),或者默认值应该更上下文敏感(在std::string s = "";中,std::string可以被读取为类型,因为其他任何东西都没有语法意义,即使std::string *s = 0;是模糊的)。再说一次,我不太清楚这些规则是如何达成一致的。我的猜测是,需要的文本页数,减轻了为哪些上下文采用类型,哪些采用非类型而创建大量特定规则的压力。