Each virtual base of a given type refers to the same object, no matter where it appears in the hierarchy.[111] This means that when a Bottom object is instantiated, the object layout may look something like this:
The Left and Right subobjects each have a pointer (or some conceptual equivalent) to the shared Top subobject, and all references to that subobject in Left and Right member functions will go through those these pointers.[112] In this case, there is no ambiguity when upcasting from a Bottom to a Top object, since there is only one Top object to convert to.
The output of the previous program is as follows:
36
1,2,3,4
1245032
1
1245060
1245032
The addresses printed suggest that this particular implementation does indeed store the Top subobject at the end of the complete object (although it’s not really important where it goes). The result of a dynamic_cast to void* always resolves to the address of the complete object.
We made the Top destructor virtual so we could apply the dynamic_cast operator. If you remove that virtual destructor (and the dynamic_cast statement so the program will compile), the size of Bottom decreases to 24 bytes. That seems to be a decrease equivalent to the size of three pointers. What gives?
It’s important not to take these numbers too literally. Other compilers we use manage only to increase the size by four bytes when the virtual constructor is added. Not being compiler writers, we can’t tell you their secrets. We can tell you, however, that with multiple inheritance, a derived object must behave as if it has multiple VPTRs, one for each of its direct base classes that also have virtual functions. It’s as simple as that. Compilers can make whatever optimizations its authors can invent, but the behavior must be the same.
Certainly the strangest thing in the previous code is the initializer for Top in the Bottom constructor. Normally one doesn’t worry about initializing subobjects beyond direct base classes, since all classes take care of initializing their own bases. There are, however, multiple paths from Bottom to Top, so relying on the intermediate classes Left and Right to pass along the necessary initialization data results in an ambiguity (whose responsibility is it?)! For this reason, it is always the responsibility of the most derived class to initialize a virtual base. But what about the expressions in the Left and Right constructors that also initialize Top? They are certainly necessary when creating standalone Left or Right objects, but must be ignored when a Bottom object is created (hence the zeros in their initializers in the Bottom constructor—any values in those slots are ignored when the Left and Right constructors execute in the context of a Bottom object). The compiler takes care of all this for you, but it’s important to understand where the responsibility lies. Always make sure that all concrete (nonabstract) classes in a multiple inheritance hierarchy are aware of any virtual bases and initialize them appropriately.
These rules of responsibility apply not only to initialization but to all operations that span the class hierarchy. Consider the stream inserter in the previous code. We made the data protected so we could "cheat" and access inherited data in operator<<(ostream&, const Bottom&). It usually makes more sense to assign the work of printing each subobject to its corresponding class and have the derived class call its base class functions as needed. What would happen if we tried that with operator<<( ), as the following code illustrates?
//: C09:VirtualBase2.cpp
// Shows how not to implement operator<<
#include <iostream>
using namespace std;
class Top {
int x;
public:
Top(int n) { x = n; }
friend ostream&
operator<<(ostream& os, const Top& t) {
return os << t.x;
}
};
class Left : virtual public Top {
int y;
public:
Left(int m, int n) : Top(m) { y = n; }
friend ostream&
operator<<(ostream& os, const Left& l) {
return os << static_cast<const Top&>(l) << ',' << l.y;
}
};
class Right : virtual public Top {
int z;
public:
Right(int m, int n) : Top(m) { z = n; }
friend ostream&
operator<<(ostream& os, const Right& r) {
return os << static_cast<const Top&>(r) << ',' << r.z;
}
};
class Bottom : public Left, public Right {
int w;
public:
Bottom(int i, int j, int k, int m)
: Top(i), Left(0, j), Right(0, k) { w = m; }
friend ostream&
operator<<(ostream& os, const Bottom& b) {
return os << static_cast<const Left&>(b)
<< ',' << static_cast<const Right&>(b)
<< ',' << b.w;
}
};
int main() {
Bottom b(1, 2, 3, 4);
cout << b << endl; // 1,2,1,3,4
} ///:~
You can’t just blindly share the responsibility upward in the usual fashion because the Left and Right stream inserters each call the Top inserter, and again there will be duplication of data. Instead you need to mimic what the compiler automatically does with initialization. One solution is to provide special functions in the classes that know about the virtual base class, which ignore the virtual base when printing (leaving the job to the most derived class):
//: C09:VirtualBase3.cpp
// A correct stream inserter
#include <iostream>
using namespace std;
class Top {
int x;
public:
Top(int n) { x = n; }
friend ostream&
operator<<(ostream& os, const Top& t) {
return os << t.x;
}
};
class Left : virtual public Top {
int y;
protected:
void specialPrint(ostream& os) const {
// Only print Left's part
os << ','<< y;
}
public:
Left(int m, int n) : Top(m) { y = n; }
friend ostream&
operator<<(ostream& os, const Left& l) {
return os << static_cast<const Top&>(l) << ',' << l.y;
}
};
class Right : virtual public Top {
111
We use the term
112
The presence of these pointers explains why the size of b is much larger than the size of four integers.В This is (part of) the cost of virtual base classes. There is also VPTR overhead due to the virtual constructor.