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 ofX
, or- if
X
is a nested class of classY
[...]- [...]
- if
X
is a member of namespaceN
, or [...], before the use of the name, in namespaceN
or in one ofN
’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.