Ambiguous base classes (C++ only)

When you derive classes, ambiguities can result if base and derived classes have members with the same names. Access to a base class member is ambiguous if you use a name or qualified name that does not refer to a unique function or object. The declaration of a member with an ambiguous name in a derived class is not an error. The ambiguity is only flagged as an error if you use the ambiguous member name.

For example, suppose that two classes named A and B both have a member named x, and a class named C inherits from both A and B. An attempt to access x from class C would be ambiguous. You can resolve ambiguity by qualifying a member with its class name using the scope resolution (::) operator.

CCNX14G

class B1 {
public:
  int i;
  int j;
  void g(int) { }
};

class B2 {
public:
  int j;
  void g() { }
};

class D : public B1, public B2 {
public:
  int i;
};

int main() {
  D dobj;
  D *dptr = &dobj;
  dptr->i = 5;
//  dptr->j = 10;
  dptr->B1::j = 10;
//  dobj.g();
  dobj.B2::g();
}

The statement dptr->j = 10 is ambiguous because the name j appears both in B1 and B2. The statement dobj.g() is ambiguous because the name g appears both in B1 and B2, even though B1::g(int) and B2::g() have different parameters.

The compiler checks for ambiguities at compile time. Because ambiguity checking occurs before access control or type checking, ambiguities may result even if only one of several members with the same name is accessible from the derived class.

Name hiding

Suppose two subobjects named A and B both have a member name x. The member name x of subobject B hides the member name x of subobject A if A is a base class of B. The following example demonstrates this:
struct A {
   int x;
};

struct B: A {
   int x;
   void f() { x = 0; }
};

int main() {
   B b;
   b.f();
}
The assignment x = 0 in function B::f() is not ambiguous because the declaration B::x has hidden A::x.
A base class declaration can be hidden along one path in the inheritance graph and not hidden along another path. The following example demonstrates this:
struct A { int x; };
struct B { int y; };
struct C: A, virtual B { };
struct D: A, virtual B {
   int x;
   int y;
};
struct E: C, D { };

int main() {
   E e;
//   e.x = 1;
   e.y = 2;
}

The assignment e.x = 1 is ambiguous. The declaration D::x hides A::x along the path D::A::x, but it does not hide A::x along the path C::A::x. Therefore the variable x could refer to either D::x or A::x. The assignment e.y = 2 is not ambiguous. The declaration D::y hides B::y along both paths D::B::y and C::B::y because B is a virtual base class.

Ambiguity and using declarations

Suppose you have a class named C that inherits from a class named A, and x is a member name of A. If you use a using declaration to declare A::x in C, then x is also a member of C; C::x does not hide A::x. Therefore using declarations cannot resolve ambiguities due to inherited members. The following example demonstrates this:
struct A {
   int x;
};

struct B: A { };

struct C: A {
   using A::x;
};

struct D: B, C {
   void f() { x = 0; }
};

int main() {
   D i;
   i.f();
}
The compiler will not allow the assignment x = 0 in function D::f() because it is ambiguous. The compiler can find x in two ways: as B::x or as C::x.

Unambiguous class members

The compiler can unambiguously find static members, nested types, and enumerators defined in a base class A regardless of the number of subobjects of type A an object has. The following example demonstrates this:
struct A {
   int x;
   static int s;
   typedef A* Pointer_A;
   enum { e };
};

int A::s;

struct B: A { };

struct C: A { };

struct D: B, C {
   void f() {
      s = 1;
      Pointer_A pa;
      int i = e;
//      x = 1;
   }
};

int main() {
   D i;
   i.f();
}
The compiler allows the assignment s = 1, the declaration Pointer_A pa, and the statement int i = e. There is only one static variable s, only one typedef Pointer_A, and only one enumerator e. The compiler would not allow the assignment x = 1 because x can be reached either from class B or class C.

Pointer conversions

Conversions (either implicit or explicit) from a derived class pointer or reference to a base class pointer or reference must refer unambiguously to the same accessible base class object. (An accessible base class is a publicly derived base class that is neither hidden nor ambiguous in the inheritance hierarchy.) For example:
class W { /* ... */ };
class X : public W { /* ... */ };
class Y : public W { /* ... */ };
class Z : public X, public Y { /* ... */ };
int main ()
{
      Z z;
      X* xptr = &z;       // valid
      Y* yptr = &z;       // valid
      W* wptr = &z;       // error, ambiguous reference to class W
                          // X's W or Y's W ?
}
You can use virtual base classes to avoid ambiguous reference. For example:
class W { /* ... */ };
class X : public virtual W { /* ... */ };
class Y : public virtual W { /* ... */ };
class Z : public X, public Y { /* ... */ };
int main ()
{
      Z z;
      X* xptr = &z;      // valid
      Y* yptr = &z;      // valid
      W* wptr = &z;      // valid, W is virtual therefore only one
                         // W subobject exists
}
A pointer to a member of a base class can be converted to a pointer to a member of a derived class if the following conditions are true:
  • The conversion is not ambiguous. The conversion is ambiguous if multiple instances of the base class are in the derived class.
  • A pointer to the derived class can be converted to a pointer to the base class. If this is the case, the base class is said to be accessible.
  • Member types must match. For example suppose class A is a base class of class B. You cannot convert a pointer to member of A of type int to a pointer to member of type B of type float.
  • The base class cannot be virtual.

Overload resolution

Overload resolution takes place after the compiler unambiguously finds a given function name. The following example demonstrates this:
struct A {
   int f() { return 1; }
};

struct B {
   int f(int arg) { return arg; }
};

struct C: A, B {
   int g() { return f(); }
};
The compiler will not allow the function call to f() in C::g() because the name f has been declared both in A and B. The compiler detects the ambiguity error before overload resolution can select the base match A::f().