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

发布于 2024-10-11 07:39:00 字数 391 浏览 7 评论 0原文

如果下面的类不是模板,我可以简单地将 x 放在 衍生 类中。但是,对于下面的代码,我必须使用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;
}

If the classes below were not templates I could simply have x in the derived class. However, with the code below, I have to use this->x. Why?

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;
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

给我一枪 2024-10-18 07:39:00

简短回答:为了使 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 的类型很可能尚未定义,即使定义了,我们也不知道哪种类型将用作模板参数。但我们必须解决语法问题,以便对格式错误的模板进行宝贵的第一阶段检查。因此,标准对依赖名称有一条规则 - 编译器必须假设它们是非类型,除非用 typename 限定以指定它们类型,或者用于某些明确的上下文。例如在 template中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< /code> 不是该定义中的成员,有人稍后可以为某种类型 Baz 定义 Bar 的特化,例如 Bar code> 确实有一个数据成员 x,然后实例化 Foo。因此,在该实例化中,您的模板将返回数据成员,而不是返回全局 x。或者相反,如果 Bar 的基本模板定义有 x,他们可以在没有它的情况下定义专门化,并且您的模板将查找全局 xFoo 中返回。我认为这被认为与您遇到的问题一样令人惊讶和痛苦,但它默默地令人惊讶,而不是抛出令人惊讶的错误。

为了避免这些问题,有效的标准规定,除非明确要求,否则不会考虑搜索类模板的依赖基类。这会阻止一切事物仅仅因为可以在依赖库中找到而依赖它。它还具有您所看到的不良效果 - 您必须限定基类中的内容,否则找不到它。使 A 依赖的常见方法有以下三种:

  • 在类中使用 Bar::A; - A 现在引用Bar,因此是依赖的。
  • 使用时的 Bar::A *x = 0; - 同样,A 肯定位于 Bar 中。这是乘法,因为没有使用 typename ,所以可能是一个坏例子,但我们必须等到实例化才能找出 operator*(Bar::A, x) 返回一个右值。谁知道,也许它确实...
  • this->A; 在使用时 - A 是一个成员,所以如果它不在 Foo 中code>,它必须位于基类中,标准再次表明这使其具有依赖性。

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

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

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

Short answer: in order to make x a dependent name, so that lookup is deferred until the template parameter is known.

Long answer: when a compiler sees a template, it is supposed to perform certain checks immediately, without seeing the template parameter. Others are deferred until the parameter is known. It's called two-phase compilation, and MSVC doesn't do it but it's required by the standard and implemented by the other major compilers. If you like, the compiler must compile the template as soon as it sees it (to some kind of internal parse tree representation), and defer compiling the instantiation until later.

The checks that are performed on the template itself, rather than on particular instantiations of it, require that the compiler be able to resolve the grammar of the code in the template.

In C++ (and C), in order to resolve the grammar of code, you sometimes need to know whether something is a type or not. For example:

#if WANT_POINTER
    typedef int A;
#else
    int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }

if A is a type, that declares a pointer (with no effect other than to shadow the global x). If A is an object, that's multiplication (and barring some operator overloading it's illegal, assigning to an rvalue). If it is wrong, this error must be diagnosed in phase 1, it's defined by the standard to be an error in the template, not in some particular instantiation of it. Even if the template is never instantiated, if A is an int then the above code is ill-formed and must be diagnosed, just as it would be if foo wasn't a template at all, but a plain function.

Now, the standard says that names which aren't dependent on template parameters must be resolvable in phase 1. A here is not a dependent name, it refers to the same thing regardless of type T. So it needs to be defined before the template is defined in order to be found and checked in phase 1.

T::A would be a name that depends on T. We can't possibly know in phase 1 whether that's a type or not. The type which will eventually be used as T in an instantiation quite likely isn't even defined yet, and even if it was we don't know which type(s) will be used as our template parameter. But we have to resolve the grammar in order to do our precious phase 1 checks for ill-formed templates. So the standard has a rule for dependent names - the compiler must assume that they're non-types, unless qualified with typename to specify that they are types, or used in certain unambiguous contexts. For example in template <typename T> struct Foo : T::A {};, T::A is used as a base class and hence is unambiguously a type. If Foo is instantiated with some type that has a data member A instead of a nested type A, that's an error in the code doing the instantiation (phase 2), not an error in the template (phase 1).

But what about a class template with a dependent base class?

template <typename T>
struct Foo : Bar<T> {
    Foo() { A *x = 0; }
};

Is A a dependent name or not? With base classes, any name could appear in the base class. So we could say that A is a dependent name, and treat it as a non-type. This would have the undesirable effect that every name in Foo is dependent, and hence every type used in Foo (except built-in types) has to be qualified. Inside of Foo, you'd have to write:

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

because std::string would be a dependent name, and hence assumed to be a non-type unless specified otherwise. Ouch!

A second problem with allowing your preferred code (return x;) is that even if Bar is defined before Foo, and x isn't a member in that definition, someone could later define a specialization of Bar for some type Baz, such that Bar<Baz> does have a data member x, and then instantiate Foo<Baz>. So in that instantiation, your template would return the data member instead of returning the global x. Or conversely if the base template definition of Bar had x, they could define a specialization without it, and your template would look for a global x to return in Foo<Baz>. I think this was judged to be just as surprising and distressing as the problem you have, but it's silently surprising, as opposed to throwing a surprising error.

To avoid these problems, the standard in effect says that dependent base classes of class templates just aren't considered for search unless explicitly requested. This stops everything from being dependent just because it could be found in a dependent base. It also has the undesirable effect that you're seeing - you have to qualify stuff from the base class or it's not found. There are three common ways to make A dependent:

  • using Bar<T>::A; in the class - A now refers to something in Bar<T>, hence dependent.
  • Bar<T>::A *x = 0; at point of use - Again, A is definitely in Bar<T>. This is multiplication since typename wasn't used, so possibly a bad example, but we'll have to wait until instantiation to find out whether operator*(Bar<T>::A, x) returns an rvalue. Who knows, maybe it does...
  • this->A; at point of use - A is a member, so if it's not in Foo, it must be in the base class, again the standard says this makes it dependent.

Two-phase compilation is fiddly and difficult, and introduces some surprising requirements for extra verbiage in your code. But rather like democracy it's probably the worst possible way of doing things, apart from all the others.

You could reasonably argue that in your example, return x; doesn't make sense if x is a nested type in the base class, so the language should (a) say that it's a dependent name and (2) treat it as a non-type, and your code would work without this->. To an extent you're the victim of collateral damage from the solution to a problem that doesn't apply in your case, but there's still the issue of your base class potentially introducing names under you that shadow globals, or not having names you thought they had, and a global being found instead.

You could also possibly argue that the default should be the opposite for dependent names (assume type unless somehow specified to be an object), or that the default should be more context sensitive (in std::string s = "";, std::string could be read as a type since nothing else makes grammatical sense, even though std::string *s = 0; is ambiguous). Again, I don't know quite how the rules were agreed. My guess is that the number of pages of text that would be required, mitigated against creating a lot of specific rules for which contexts take a type and which a non-type.

千年*琉璃梦 2024-10-18 07:39:00

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

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


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

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

我不知道“因为标准是这样说的”是否算作答案。我们现在可以问为什么标准要求这样做,但正如 Steve Jessop 的出色答案 和其他人指出的那样,后一个问题的答案相当长且有争议。不幸的是,当谈到 C++ 标准时,通常几乎不可能给出一个简短且独立的解释来解释为什么该标准要求某些东西;这也适用于后一个问题。

(Original answer from Jan 10, 2011)

I think I have found the answer: GCC issue: using a member of a base class that depends on a template argument.
The answer is not specific to gcc.


Update: In response to mmichael's comment, from the draft N3337 of the C++11 Standard:

14.6.2 Dependent names [temp.dep]
[...]
3 In the definition of a class or class template, if a base class depends on a
template-parameter, the base class scope is not examined during unqualified name
lookup either at the point of definition of the class template
or member or during an instantiation of the class template or member.

Whether "because the standard says so" counts as an answer, I don't know. We can now ask why the standard mandates that but as Steve Jessop's excellent answer and others point out, the answer to this latter question is rather long and arguable. Unfortunately, when it comes to the C++ Standard, it is often nearly impossible to give a short and self-contained explanation as to why the standard mandates something; this applies to the latter question as well.

最近可好 2024-10-18 07:39:00

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

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

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

The x is hidden during the inheritance. You can unhide via:

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

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文