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

当前回答

(原答案来自2011年1月10日)

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


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

14.6.2依赖名称[temp.dep] […] 在类或类模板的定义中,如果基类依赖于 模板参数时,基类作用域在非限定名称期间不检查 在类模板的定义点进行查找 或类模板或成员的实例化过程中。

我不知道“因为标准这么说”算不算答案。我们现在可以问为什么标准要求这样做,但正如史蒂夫·杰索普的精彩回答和其他人指出的那样,后一个问题的答案相当长,而且有争议。不幸的是,当涉及到c++标准时,几乎不可能给出一个简短的、自成体系的解释来解释为什么标准要求某些东西;这同样适用于后一个问题。

其他回答

(原答案来自2011年1月10日)

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


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

14.6.2依赖名称[temp.dep] […] 在类或类模板的定义中,如果基类依赖于 模板参数时,基类作用域在非限定名称期间不检查 在类模板的定义点进行查找 或类模板或成员的实例化过程中。

我不知道“因为标准这么说”算不算答案。我们现在可以问为什么标准要求这样做,但正如史蒂夫·杰索普的精彩回答和其他人指出的那样,后一个问题的答案相当长,而且有争议。不幸的是,当涉及到c++标准时,几乎不可能给出一个简短的、自成体系的解释来解释为什么标准要求某些东西;这同样适用于后一个问题。

简单回答:为了使x成为依赖名称,查找将被推迟到已知模板形参为止。

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.

对模板本身执行的检查,而不是对模板的特定实例化执行的检查,要求编译器能够解析模板中代码的语法。

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

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.

现在,标准规定,不依赖于模板参数的名称必须在阶段1中可解析。这里的A不是一个依赖名称,它指的是同一个东西,不管类型是t,所以需要在定义模板之前定义它,以便在阶段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).

但是带有依赖基类的类模板呢?

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

A是否是从属名称?对于基类,任何名称都可以出现在基类中。因此,我们可以说A是一个依赖名称,并将其视为非类型。这将产生不良影响,Foo中的每个名称都是依赖的,因此Foo中使用的每个类型(内置类型除外)都必须是限定的。在Foo内部,你必须这样写:

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

因为std::string将是一个依赖名称,因此假定为非类型,除非另有指定。哎哟!

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.

为了避免这些问题,该标准实际上规定除非明确请求,否则类模板的依赖基类不会被考虑用于搜索。这使得所有东西都不再是依赖的,因为它可以在依赖基中找到。它也有您所看到的不良影响-您必须从基类中限定内容,否则就找不到它。有三种常见的方法使A依赖:

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.

两阶段编译既精细又困难,并且在代码中引入了一些令人惊讶的额外冗余要求。但就像民主一样,它可能是最糟糕的做事方式。

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.

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

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

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