Access modifier's different behaviors in inheritance depend on "this" keyword and templates or lack thereof

StackOverflow https://stackoverflow.com/questions/20808993

Pregunta

I want to understand the access modifiers' 4 different behaviors regarding inheritance when it comes to the 4 combinations of using and/or omitting templates and the this keyword. All following code is done in g++ 4.8:

Here's a GrandChild class, which privately inherits from Parent, which privately 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 privateness 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?

¿Fue útil?

Solución

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.

Otros consejos

For a good explanation of why private inheritance is not like public and protected inheritance, see two good answers from private inheritance.

From a common understanding of inheritance, C++’ “private inheritance” is a horrible misnomer: it is not inheritance (as far as everything outside of the class is concerned) but a complete implementation detail of the class.

Seen from the outside, private inheritance is actually pretty much the same as composition. Only on the inside of the class do you get special syntax that is more reminiscent of inheritance than composition.

There’s a caveat though: C++ syntactically treats this as inheritance, with all the benefits and problems that this entails, such as scope visibility and accessibility. Furthermore, C-style casts (but no C++ cast!) actually ignores visibility and thus succeeds in casting your Derived pointer to Base:

Base* bPtr = (Base*) new Derived();

Needless to say, this is evil.


Public inheritance means that everyone knows that Derived is derived from Base.

Protected inheritance means that only Derived, friends of Derived, and classes derived from Derived know that Derived is derived from Base.*

Private inheritance means that only Derived and friends of Derived know that Derived is derived from Base.

Since you have used private inheritance, your main() function has no clue about the derivation from base, hence can't assign the pointer.

Private inheritance is usually used to fulfill the "is-implemented-in-terms-of" relationship. One example might be that Base exposes a virtual function that you need to override -- and thus must be inherited from -- but you don't want clients to know that you have that inheritance relationship.

As well as Inaccessible type due to private inheritance.

This is due to the injected class name from A hiding the global A inside C. Although A is visible, it is not accessible (since it is imported as private), hence the error. You can access A by looking it up in the global namespace:

void foo(::A const& a) {}

So for example, this will work:

class GrandChild;
struct GrandParent { enum {n = 0}; };

struct Parent : private GrandParent { 
   enum {n = 1}; 
   friend GrandChild; 
};

struct GrandChild : private Parent {
    void f() {cout << GrandParent::n << endl;}
};

Otherwise you need to use global scope or the using directive to bring ::GrandParent into scope.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top