Access modifier's different behaviors in inheritance depend on "this" keyword and templates or lack thereof
access-modifiers, c++, inheritance, templates, this
Solution
The whole issue here is name lookup (I think this also has been the case in one of your previous questions). I'll try to illustrate my understanding of what's happening:
Every (named) class gets an injected-class-name. For example:
struct GrandParent
{
// using GrandParent = ::GrandParent;
enum {n = 0};
};
You can use this injected-class-name to refer to the class itself. It is not that useful for ordinary classes (where unqualified lookup could find the name `GrandParent` in the surrounding scope anyway), but for derived classes and class templates:
namespace A
{
struct Foo
{
// using Foo = ::A::Foo;
};
};
struct Bar : A::Foo
{
void woof(Foo); // using the injected-class-name `::A::Foo::Foo`
};
template<class T, int N, bool b>
struct my_template
{
// using my_template = ::my_template<T, N, b>;
void meow(my_template); // using the injected-class-name
};
This isn't inheritance as in "it's part of the base class subobject", but the way unqualified lookup is specified: If the name isn't found in the current class' scope, the base class scopes will be searched.
Now, for the first (non-template) example in the OP:
struct Parent : private GrandParent
{
// using Parent = ::Parent;
enum {n = 1}; // hides GrandParent::n
};
struct GrandChild : private Parent {
// using GrandChild = ::GrandChild;
enum {n = 2};
void f() {cout << GrandParent::n << endl;}
// ^ error: 'struct GrandParent GrandParent::GrandParent'
// is inaccessible
};
Here, the expression `GrandParent::n` invokes unqualified name lookup of the name `GrandParent`. As unqualified lookup stops when the name is found (and doesn't consider surrounding scopes), it will find the injected-class-name `GrandParent::GrandParent`. That is, lookup searches the scope of `GrandChild` (name not found), then the scope of `Parent` (name not found) and finally the scope of `GrandParent` (where it finds the injected-class-name). This is done before and independent of access checking.
After the name `GrandParent` has been found, accessibility is checked eventually. Name lookup required to go from `Parent` to `GrandParent` to find the name. This path is blocked for anyone but members and friends of `Parent`, as the inheritance is private. (You can see through that path, but you may not use it; visibility and accessibility are orthogonal concepts.)
Here's the standardese [basic.lookup.unqual]/8:
For the members of a class `X`, a name used in a member function body [...] shall be declared in one of the following ways:
- before its use in the block in which it is used or in an enclosing block, or
- shall be a member of class `X` or be a member of a base class of `X`, or
- if `X` is a nested class of class `Y` [...]
- [...]
- if `X` is a member of namespace `N`, or [...], before the use of the name, in namespace `N` or in one of `N`’s enclosing namespaces.
Name lookup in base classes is rather complicated as multiple base classes might have to be considered. For single inheritance and a member looked up in the scope of a member function body, it starts with the class which this function is a member of, and then traverses the base classes up (base, base of base, base of base of base, ..). See [class.member.lookup]
The template case is different, as `bar` is the name of a class template:
template <unsigned int N>
struct bar : private bar<N - 1> {
enum {num = N};
void g() {
static_assert(N >= 2, "range error");
cout << bar<N - 2>::num << endl;
}
};
Here, `bar<N - 2>` is used. It is a dependent name, as `N` is a template parameter. Name lookup is therefore postponed until the point of instantiation of `g`. The specialization `bar<0>` can be found, even it is declared after the function.
The injected-class-name of `bar` can be used as a template-name (referring to the class template) or as a type-name (referring to the current instantiation) [temp.local]/1:
Like normal (non-template) classes, class templates have an injected-class-name (Clause 9). The injected- class-name can be used as a template-name or a type-name. When it is used with a template-argument-list, as a template-argument for a template template-parameter, or as the final identifier in the elaborated-type- specifier of a friend class template declaration, it refers to the class template itself. Otherwise, it is equivalent to the template-name followed by the template-parameters of the class template enclosed in `<>`.
That is, `bar<N - 2>` finds `bar` as the injected-class-name of the current class (instantiation). As it is used with a template-argument-list, it refers to another, unrelated specialization of `bar`. The injected-class-name of the base class is hidden.
`bar<0>::num` is accessed not through an access path that goes through a private inheritance, but directly through the injected-class-name of the current class, referring to the class template itself. `num` being a public member of `bar<0>` is accessible.
Problem
I want to understand the access modifiers' 4 different behaviors regarding inheritance when it comes to the 4 combinations of using and/or omitting `template`s and the `this` keyword. All following code is done in g++ 4.8: Here's a `GrandChild` class, which `private`ly inherits from `Parent`, which `private`ly inherits from `GrandParent`, which has a `public` `enum` `n`. Non-object, client code can access `GrandParent::n`, because the latter is a `public` `enum`. But `GrandParent::n` is inaccessible from within `GrandChild`: ``` #include <iostream> using namespace std; struct GrandParent { enum {n = 0}; }; struct Parent : private GrandParent { enum {n = 1}; }; struct GrandChild : private Parent { enum {n = 2}; void f() {cout << GrandParent::n << endl;} // ^ error: 'struct GrandParent GrandParent::GrandParent' // is inaccessible }; int main() { cout << GrandParent::n << endl; // ^ non-object access would have outputted `0` had `GrandChild`'s // definition compiled or been commented out. } ``` 1.) Is `GrandParent::n`'s inaccessibility from within `GrandChild` caused by `GrandChild`'s possession of a `GrandParent` base subobject, which hides non-object access to `GrandParent::num`, and whose 2-generational `private`ness makes the base subobject’s `n` also inaccessible? I'd expected the error message to be about that. 2.) But apparently, it isn't. Why does the error complain about `GrandParent`'s constructor? 3.) Prepending `this->` to `GrandParent::n` in `f()`'s definition will add the error I expected in #1 but won't remove the ctor complaint. Why? I assumed that including `this->` is redundant and that its omission will cause the lookup to attempt to find the `n` of the `GrandParent` subobject within `GrandChild`'s scope before the less-immediately-scoped non-object `n` anyway. 4.) Why does this template variant compile? It seems functionally similar to the non-template one: ``` #include <iostream> using namespace std; template <unsigned int N> struct bar : private bar<N - 1> { enum {num = N}; void g() { static_assert(N >= 2, "range error"); cout << bar<N - 2>::num << endl; } }; template <> struct bar<0> { enum {num = 0}; }; int main() { bar<2> b2; b2.g(); // Output: 0 } ``` 5.) Prepending `this->` to `bar<N - 2>::num` in `g()`'s definition causes the compiler error I expected in #1 only. But why doesn't it include the error of #2? And why doesn't its omission yield #2's error?