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

标签 c++ templates inheritance 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;
}

最佳答案

简答:为了使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 中的每个名称都是相关的,因此 Foo 中使用的每个类型(内置类型除外)都必须进行限定。在 Foo 里面,你必须写:
typename std::string s = "hello, world";

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

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

为了避免这些问题,该标准实际上表示,除非明确要求,否则不会考虑搜索类模板的依赖基类。这阻止了一切都依赖,仅仅因为它可以在依赖库中找到。它还具有您所看到的不良影响 - 您必须从基类中限定内容,否则找不到。 A常见的制作方法有3种依赖:
  • using Bar<T>::A;在类里面 - A现在指的是 Bar<T> 中的某事,因此依赖。
  • Bar<T>::A *x = 0;在使用点 - 再次,A绝对在Bar<T> .这是自 typename 以来的乘法没有使用,所以可能是一个不好的例子,但我们必须等到实例化才能找出 operator*(Bar<T>::A, x)返回一个右值。谁知道呢,也许真的...
  • this->A;在使用点 - A是成员(member),所以如果它不在 Foo ,它必须在基类中,再次标准说这使它依赖。

  • 两阶段编译既繁琐又困难,并且在您的代码中引入了一些令人惊讶的额外冗长要求。但就像民主一样,它可能是最糟糕的做事方式,除了所有其他方式。

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

    您也可能会争辩说,依赖名称的默认值应该相反(假设类型,除非以某种方式指定为对象),或者默认值应该更加上下文敏感(在 std::string s = ""; 中, std::string 可以读作类型,因为没有其他任何语法意义,即使 std::string *s = 0; 是模棱两可的)。同样,我不太清楚规则是如何达成一致的。我的猜测是,需要的文本页数可以减少为上下文采用类型和非类型创建许多特定规则。

    关于c++ - 为什么我必须通过 this 指针访问模板基类成员?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4643074/

    相关文章:

    C++这里调用的是拷贝构造函数吗?

    c++ - 命名空间类的操作重载

    C++ 模板 t 不是有效的模板类型

    python - 比较 web.py 的 Templator 和 Jinja2 : strengths and weaknesses

    c# - 访问基类重写 ToString 方法中的子类对象

    c++ - C++继承类函数 : general questions for better understanding and if someone can point out my mistakes

    c# - 每个继承的类的C#静态实例成员

    c++ - Qwtslider 布局 - 显示当前值

    c++ - 与 Windows 重新启动/关闭的交互

    c# - 在 StringTemplate 中分组