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

Ali*_*Ali 184 c++ inheritance templates c++-faq

如果下面的类不是模板,我可以简单地xderived课堂上.但是,使用下面的代码,我必须使用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;
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*sop 257

简答:为了创建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; }
Run Code Online (Sandbox Code Playgroud)

如果A是一个类型,则声明一个指针(除了影响全局之外没有任何效果x).如果A是一个对象,那就是乘法(并且禁止某些运算符重载它是非法的,分配给一个rvalue).如果错误,则必须在阶段1中诊断此错误,它由标准定义为模板中的错误,而不是在某个特定的实例化中.即使模板从未实例化,如果A是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; }
};
Run Code Online (Sandbox Code Playgroud)

是一个受抚养的名字?对于基类,任何名称都可以出现在基类中.所以我们可以说A是一个从属名称,并将其视为非类型.这会产生不良影响,即Foo 中的每个名称都是依赖的,因此Foo中使用的每种类型(内置类型除外)都必须是合格的.在Foo里面,你必须写:

typename std::string s = "hello, world";
Run Code Online (Sandbox Code Playgroud)

因为它std::string是一个从属名称,因此除非另有说明,否则假定为非类型.哎哟!

允许您的首选代码(return x;)的第二个问题是,即使Bar之前已定义Foo,并且x不是该定义中的成员,某人可以稍后Bar为某种类型定义特殊化Baz,例如Bar<Baz>具有数据成员x,然后实例化Foo<Baz>.因此,在该实例化中,您的模板将返回数据成员而不是返回全局x.或者相反,如果的基本模板定义Barx,他们可以定义一个专业化,没有它,你的模板将寻求一个全球性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是基类中的嵌套类型,则没有意义,因此语言应该(a)说它是一个依赖名称并且(2)将其视为非类型,并且你的代码可以不用this->.在某种程度上,你是解决问题的附带损害的受害者,这个问题不适用于你的情况,但是你的基类仍然可能会在你的名下引入阴影全局,或者没有你想到的名字他们有了,而且找到了一个全球性的人.

您也可能认为默认值应该与依赖名称相反(假设类型除非以某种方式指定为对象),或者默认值应该更加上下文敏感(in std::string s = "";,std::string可以作为一种类型读取,因为没有别的东西可以使用语法感觉,即使std::string *s = 0;是模棱两可的).我再也不知道规则是如何达成一致的.我的猜测是需要的文本页面数,减少了为上下文创建类型和非类型创建大量特定规则的情况.

  • @jalf:有没有像C++ QTWBFAETYNSYEWTKTAAHMITTBGOW这样的东西 - "除了你不确定你甚至不想知道答案并且还有更重要的事情需要继续使用时,会经常被问到的问题"? (18认同)
  • 非凡的答案,想知道问题是否适合常见问题. (4认同)
  • 哦,好详细的答案。澄清了一些我从来没有费心去查的事情。:) +1 (2认同)

chr*_*ock 12

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

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

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

  • 这个答案并没有解释为什么*它被隐藏了. (24认同)

Ali*_*Ali 12

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

我想我找到了答案:GCC问题:使用依赖于模板参数的基类成员.答案并非针对gcc.


更新:回应mmichael的评论,来自C++ 11标准的N3337草案:

14.6.2从属名称[temp.dep]
[...]
3在类或类模板的定义中,如果基类依赖于模板参数,则在非限定名称查找期间不会检查基类范围类模板或成员的定义点,或者在类模板或成员的实例化过程中.

无论是"因为标准是这样说的"算作一个答案,我不知道.我们现在可以问为什么标准要求,但正如Steve Jessop的优秀答案和其他人所指出的那样,后一个问题的答案相当长且可论证.不幸的是,当涉及到C++标准时,通常几乎不可能给出标准强制要求的简短而自成一体的解释; 这也适用于后一个问题.