提问人:Ali 提问时间:1/10/2011 最后编辑:DestructorAli 更新时间:10/4/2019 访问量:31702
为什么我必须通过 this 指针访问模板基类成员?
Why do I have to access template base class members through the this pointer?
问:
如果下面的类不是模板,我可以简单地在课堂上使用。但是,使用下面的代码,我必须使用 .为什么?x
derived
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;
d.f();
return 0;
}
答:
在继承过程中是隐藏的。您可以通过以下方式取消隐藏:x
template <typename T>
class derived : public base<T> {
public:
using base<T>::x; // added "using" statement
int f() { return x; }
};
评论
base<T> is not a namespace or unscoped enum
(原答案来自2011年1月10日)
我想我已经找到了答案:GCC 问题:使用依赖于模板参数的基类的成员。 答案并不特定于 gcc。
更新:作为对 mmichael 评论的回应,来自 C++11 标准的 N3337 草案:
14.6.2 依赖名称 [temp.dep]
[...]
3 在类或类模板的定义中,如果基类依赖于 template-parameter,则在非限定名称期间不检查基类范围 在类模板的定义点查找 或成员,或在类模板或成员的实例化期间。
“因为标准这么说”算不算答案,我不知道。我们现在可以问为什么标准要求这样做,但正如史蒂夫·杰索普(Steve Jessop)和其他人的出色回答所指出的那样,后一个问题的答案相当长且值得争论。不幸的是,当涉及到 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 是一个类型,则声明一个指针(除了遮蔽全局之外没有其他作用)。如果 A 是一个对象,那就是乘法(除非某些运算符重载它是非法的,赋值给 rvalue)。如果错误,则必须在第 1 阶段诊断此错误,标准将其定义为模板中的错误,而不是模板的某些特定实例中的错误。即使模板从未实例化,如果 A 是 A,则上述代码格式不正确,必须进行诊断,就像根本不是模板,而是一个普通函数一样。x
int
foo
现在,该标准规定,不依赖于模板参数的名称必须在第 1 阶段可解析。 这里不是一个依赖名称,它指的是同一事物,无论类型如何。因此,需要在定义模板之前定义它,以便在第 1 阶段找到并检查。A
T
T::A
将是一个依赖于 T 的名称。在第一阶段,我们不可能知道这是否是一种类型。最终将在实例化中使用的类型很可能还没有定义,即使定义了,我们也不知道哪些类型将用作我们的模板参数。但是我们必须解决语法问题,以便对格式错误的模板进行宝贵的第 1 阶段检查。因此,该标准对依赖名称有一个规则 - 编译器必须假定它们是非类型,除非限定指定它们是类型,或者在某些明确的上下文中使用。例如,在 中用作基类,因此明确地是一种类型。如果使用具有数据成员而不是嵌套类型 A 的某种类型进行实例化,则这是执行实例化的代码中的错误(阶段 2),而不是模板中的错误(阶段 1)。T
typename
template <typename T> struct Foo : T::A {};
T::A
Foo
A
但是,具有依赖基类的类模板呢?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
A 是不是依赖名称?使用基类时,任何名称都可以出现在基类中。因此,我们可以说 A 是一个依赖名称,并将其视为非类型。这将产生不良效果,即 Foo 中的每个名称都是依赖的,因此 Foo 中使用的每个类型(内置类型除外)都必须经过限定。在 Foo 内部,你必须写:
typename std::string s = "hello, world";
因为 将是一个依赖名称,因此除非另有说明,否则假定为非类型。哎哟!std::string
允许首选代码 () 的第二个问题是,即使之前定义了 ,并且不是该定义中的成员,有人也可以稍后为某种类型定义 的专用化,例如确实具有数据成员,然后实例化。因此,在该实例化中,模板将返回数据成员,而不是返回全局 .或者相反,如果 had 的基本模板定义,他们可以在没有它的情况下定义专用化,并且您的模板将查找要返回的全局。我认为这被认为和你遇到的问题一样令人惊讶和痛苦,但它是默默地令人惊讶,而不是抛出一个令人惊讶的错误。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
两阶段编译既繁琐又困难,并且对代码中的额外措辞提出了一些令人惊讶的要求。但就像民主一样,它可能是最糟糕的做事方式,除了所有其他方式。
你可以合理地争辩说,在你的示例中,如果是基类中的嵌套类型,则没有意义,因此语言应该 (a) 说它是一个依赖名称,并且 (2) 将其视为非类型,并且您的代码可以在没有 .在某种程度上,你是因解决一个不适用于你的情况的问题而造成的附带损害的受害者,但仍然存在一个问题,即你的基类可能会在你下面引入影子全局名称,或者没有你认为它们有的名称,而是找到一个全局名称。return x;
x
this->
你也可能争辩说,对于依赖名称,默认值应该相反(假设类型,除非以某种方式指定为对象),或者默认值应该更上下文相关(在 中,可以被理解为一种类型,因为没有其他任何语法意义,即使模棱两可)。同样,我不太清楚这些规则是如何达成的。我的猜测是,所需的文本页数减轻了为哪些上下文采用类型和哪些非类型创建许多特定规则的情况。std::string s = "";
std::string
std::string *s = 0;
评论
x
this->
base<T>::x
using base<T>::x