6

I'm seeing something I can't explain in the following code. Under VS6, VS9, and GCC T2::foo2() gives the error: 'bar' : cannot access protected member declared in class 'C1'. But if you remove C1::bar(), it compiles and runs correctly, even though T2 is still accessing the protected C1B:bar(), which you would think would be the same problem.

Note, that in T2::foo2() you could cast 'pT1' to be a 'T1*' and everything is fine, but that still does not explain why C1B::bar() is allowed, but C1::bar() is not.

template<class S> class T2;

template<class T> class T1
{
    //template<class T> friend class T2;  --> this doesn't compile under VS6
    friend class T2<T>;

    protected:
        virtual void bar() { printf("T1\n"); }
};

template<class S> class T2
{
    public:
        void foo1(T1<S> *pT1) { pT1->bar(); }  // --> ok, makes sense, this works either way
        void foo2(S *pT1) { pT1->bar(); }  // --> this fails to compile if C1::bar() is defined, but works for C1B::foo() ???
};

class C1 : public T1<C1>
{
    protected:
        virtual void bar() { printf("C1\n"); }  // --> comment this out and foo2 will compile
};

class C1B : public  C1
{
    protected:
        virtual void bar() { printf("C1B\n"); }
};

class C2 : public  T2<C1>
{
};

void test(void)
{
    C1B c1b;
    C2 c2;
    c2.foo1(&c1b);
    c2.foo2(&c1b);  // --> fails to compile if C1::bar() exists
}
Greg
  • 63
  • 3

1 Answers1

9

In your example, the type of the parameter S in foo2 is C1. The friend relationship exists between T2<S> and T1<S>. Although C1 derives from T1<C1> it does not become a friend of all the friends of T1<C1>.

In the expression pT1->bar, bar is found in C1 and, as the friendship is not passed down to the derived class, it is private.

Your example is using virtual functions so this can be addressed by explicitly referring to the bar that is a friend of our class:

void foo2(S *pT1) { pT1->template T1<S>::bar(); }

The access check now succeeds.

The reference for this in the '03 C++ standard is in 11.4/10, where it simply says:

Friendship is neither inherited nor transitive.

Thanks to Potatoswatter for his comment. By using the qualified-id for the id-expession, we disable virtual dispatch (5.2.2/1):

If the selected function is non-virtual, or if the id-expression in the class member access expression is a qualified-id, that function is called.

We can add a non-virtual dispatcher, or we can first convert the parameter to the base type and then make the call:

 void foo2(S *pT1) { static_cast< T1<S>* > (pT1)->bar(); }

Virtual dispatch now takes place as required.

Richard Corden
  • 21,389
  • 8
  • 58
  • 85
  • 2
    +1 just for the zany function call syntax, but this defeats the `virtual` functionality which OP probably wants. Would be better to implement a non-virtual dispatcher function which is accessible by friends and serves to call a private virtual `bar`. – Potatoswatter May 11 '11 at 18:10
  • @Potatoswatter: My initial reaction was that this was the case too - however, my test with g++ showed it did the virtual dispatch. I'll try to dig this out of the standard. – Richard Corden May 11 '11 at 18:33
  • @Potatoswatter: DOH! The OP has two calls to the function, when I tested I saw the output from the first and assumed that my change worked. Thanks for pointing this out. – Richard Corden May 11 '11 at 18:44
  • Thanks for you replies. It was actually a two part question, which I tried to point out, but maybe wasn't clear. The first was that I was really interested in getting the foo2() version, which I think you answered. But the second, actually more interesting question, is why does everything in the example code compile (as-is) if you comment out C1::bar()? It would seem that if "Friendship is neither inherited nor transitive" then certainly T2::foo2() could not call C1B::bar()... but alas, it does, on all three compilers. – Greg May 12 '11 at 11:35
  • @Greg: Right so with 'C1::bar' declared, the compiler finds *that* bar, checks if we have appropriate access to that name and, since we don't, it fails to compile. The key is that access checking takes place on the name found **not** on the function eventually called by the virtual dispatch. C1B::bar is only called after we've checked access to the name in the base class. – Richard Corden May 12 '11 at 11:45