Search code examples
c++c++11c++14memberc++03

Ambiguity in Member Name LookUp and Access Declarations in C++


class A                      { public: int a;       };                 
class B : public virtual A   { public: using A::a;  };                 
class C : public virtual A   { public: using A::a;  };                 
class D : public C, public B {                      };                 

class W                      { public: int w;       };                  
class X : public virtual W   { public: using W::w;  };                  
class Y : public virtual W   {                      };                  
class Z : public Y, public X {                      };

int main(){

    D d;
    d.a = 0; // Error

    Z z;                                                               
    z.w = 0; // Correct

    return 0;
}        

The first group of class declarations (A, B, C and D) and the second (W, X, Y and Z) are built similarly except that class C has a using declaration (using A::a) and class Y doesn't.

When trying to access member a in d.a = 0 I get an error in Clang (error: member 'a' found in multiple base classes of different types) and in GCC (error: request for member 'a' is ambiguous). I checked recent and old versions of both compilers and all of them agree. However, accessing w in z.w = 0 compiles successfully.

What is the reason of this ambiguity when accessing a?

To the best of my knowledge, both access declarations in classes B and C refer to the same base class member. And by the way, if I remove them the test compiles successfully because a is already publicly accessible ( public access specifier ).

Thanks in advance.

Note: The above code is a slightly modified test from SolidSands' SuperTest suite.


Solution

  • There is implementation variance here; ICC accepts your code while gcc, clang and MSVC reject it. ICC is correct and the other compilers are incorrect.

    Running the [class.member.lookup] algorithm for D::a, we find that:

    • There is no declaration of a in D, so S(a, D) is initially empty and we merge in the lookup sets of a in its base classes, calculated as follows:
      • S(a, B) = { { A::a }, { B } }
      • S(a, C) = { { A::a }, { C } }
    • The resulting lookup set is S(a, D) = { { A::a }, { B, C } }

    Note that in the declaration set of S(a, B), the member is A::a even though it is found in B, and similarly for S(a, C):

    In the declaration set, using-declarations are replaced by the set of designated members [...]

    To determine whether the member access d.a is ambiguous, we now check [expr.ref]/5:

    5 - [The] program is ill-formed if the class of which E2 is directly a member is an ambiguous base of the naming class of E2 [...]

    Here E2 has been determined to be A::a, a direct member of A. The naming class is D. A is not an ambiguous base of D, since A is a virtual base of all the intermediate base class subobjects of D. So d.a is unambiguous both in name lookup and in member access, and your program is correct.


    As an analogous instance, we can consider replacing virtual inheritance with a static member per the note to [class.member.lookup]/9:

    9 - [ Note: A static member, a nested type or an enumerator defined in a base class T can unambiguously be found even if an object has more than one base class subobject of type T. Two base class subobjects share the non-static member subobjects of their common virtual base classes. — end note ]

    struct A { static int a; };                 
    struct B : A { using A::a; };                 
    struct C : A { using A::a; };                 
    struct D : B, C { };                 
    
    int main() {
        D d;
        d.a = 0; // OK
    }
    

    Here again we have S(a, D) = { { A::a }, { B, C } }. Indeed, name lookup proceeds the same way even if A::a were a non-static member of a non-virtual base; the name lookup is unambiguous but the member access [expr.ref] is ambiguous in that case.

    To further elucidate the distinction between name lookup and member access, consider that even for a non-static data member of a non-virtual multiply inherited base class, it is possible to unambiguously use the name of the member taking the derived class as the naming class:

    struct A { int a; };
    struct B : A { using A::a; };
    struct C : A { using A::a; };
    struct D : B, C { };
    int B::*p = &D::i;      // OK, unambiguous
    

    Unfortunately, every compiler I have tried rejects this despite this being (modulo using-declarations) an example in the Standard!