The initialization of g requires its E and F part to first be initialized, but the B and C subobjects are initialized first, because they are virtual bases, and are initialized from G’s initializer, G being the most-derived class. The class B has no base classes, so according to rule 3, its member object m is initialized, then its constructor prints "B from G", and similarly for the C subject of E. The E subobject requires A, B, and C subobjects. Since B and C have already been initialized, the A subobject of the E subobject is initialized next, and then the E subobject itself. The same scenario repeats for g’s F subobject, but without duplicating the initialization of the virtual bases.
Name lookup issues
The ambiguities we have illustrated with subobjects apply, of course, to any names, including function names. If a class has multiple direct base classes that share member functions of the same name, and you call one of those member functions, the compiler doesn’t know which one to choose. The following sample program would report such an error.
// C09:AmbiguousName.cpp
class Top {};
class Left : virtual public Top {
public:
void f(){}
};
class Right : virtual public Top {
public:
void f(){}
};
class Bottom : public Left, public Right {};
int main() {
Bottom b;
b.f(); // error here
}
The class Bottom has inherited two functions of the same name (the signature is irrelevant, since name lookup occurs before overload resolution), and there is no way to choose between them. The usual technique to disambiguate the call is to qualify the function call with the base class name:
//: C09:BreakTie.cpp
class Top {};
class Left : virtual public Top {
public:
void f(){}
};
class Right : virtual public Top {
public:
void f(){}
};
class Bottom : public Left, public Right {
public:
using Left::f;
};
int main() {
Bottom b;
b.f(); // calls Left::f()
} ///:~
The name Left::f is now found in the scope of Bottom, so the name Right::f is not even considered. Of course, if you want to introduce extra functionality beyond what Left::f( ) provides, you would implement a Bottom::f( ) function that calls Left::f( ), in addition to other things.
Functions with the same name occurring in different branches of a hierarchy often conflict. The following hierarchy has no such problem:
//: C09:Dominance.cpp
class Top {
public:
virtual void f() {}
};
class Left : virtual public Top {
public:
void f(){}
};
class Right : virtual public Top {
};
class Bottom : public Left, public Right {};
int main() {
Bottom b;
b.f(); // calls Left::f()
} ///:~
In this case, there is no explicit Right::f( ), so Left::f( ), being the most derived, is chosen. Why? Well, pretend that Right did not exist, giving the single-inheritance hierarchy Top <= Left <= Bottom. You would certainly expect Left::f( ) to be the function called by the expression b.f( ), because of normal scope rules (a derived class is considered a nested scope of a base class). In general, a name A::f is said to
The following program further illustrates the dominance principle.
//: C09:Dominance2.cpp
#include
using namespace std;
class A {
public:
virtual void f() {cout << "A::f\n";}
};
class B : virtual public A {
public:
void f() {cout << "B::f\n";}
};
class C : public B {};
class D : public C, virtual public A {};
int main()
{
B* p = new D;
p->f(); // calls B::f()
} ///:~
The class diagram for this hierarchy is as follows.
The class A is a (direct, in this case) base class for B, and so the name B::f dominates A::f.
Avoiding MI
When the question of whether to use multiple inheritance comes up, ask at least two questions:
1.Do you need to show the public interfaces of both these classes through your new type, or could one class be contained within the other, with only some of its interface exposed in the new class?
2.Do you need to upcast to both of the base classes? (This applies when you have more than two base classes, of course.)
If you can answer "yes" to either question, you can avoid using MI and should probably do so.
One situation to watch for is when one class needs to be upcast only as a function argument. In that case, the class can be embedded and an automatic type conversion operator provided in your new class to produce a reference to the embedded object. Any time you use an object of your new class as an argument to a function that expects the embedded object, the type conversion operator is used.[114] However, type conversion can’t be used for normal member selection; that requires inheritance.
Extending an interface