小编典典

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

all

如果下面的类不是模板,我可以简单地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;
}

阅读 55

收藏
2022-05-30

共1个答案

小编典典

简短的回答:为了创建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 是
anint那么上面的代码格式错误并且必须进行诊断,就像根本foo不是模板而是普通函数一样。

现在,标准规定 依赖于模板参数的名称在阶段 1 中必须是可解析的。A这里不是依赖名称,它指的是同一个东西,与 type
无关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;是,即使Bar之前定义过Foo,并且x不是该定义中的成员,也可以稍后Bar为某些类型定义一个特化Baz,这样它Bar<Baz>确实有一个数据成员x,然后实例化Foo<Baz>.
因此,在该实例化中,您的模板将返回数据成员而不是返回全局x. 或者相反,如果基本模板定义Barhad
x,他们可以定义一个没有它的专业化,并且您的模板将寻找一个全局x返回 in
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->.
在某种程度上,您是对不适用于您的案例的问题的解决方案造成的附带损害的受害者,但仍然存在您的基类可能会在您下面引入影子全局名称的问题,或者没有您认为的名称他们有,而是找到了一个全球性的存在。

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

2022-05-30