ap == 1245052
bp == 1245056
cp == 1245052
bp == cp? true
0
*/ ///:~
As you can see, the B portion of the object c is offset 4 bytes from the beginning of the entire object, suggesting the following layout:
The object c begins with it’s A subobject, then the B portion, and finally the data from the complete type C itself. Since a C is-an A and is-a B, it is possible to upcast to either base type. When upcasting to an A, the resulting pointer points to the A portion, which happens to be at the beginning of the C object, so the address ap is the same as the expression &c. When upcasting to a B, however, the resulting pointer must point to where the B subobject actually resides, because class B knows nothing about class C (or class A, for that matter). In other words, the object pointed to by bp must be able to behave as a standalone B object (except for any required polymorphic behavior, of course).
When casting bp back to a C*, since the original object was a C in the first place, the location where the B subobject resides is known, so the pointer is adjusted back to the original address of the complete object. If bp had been pointing to a standalone B object instead of a C object in the first place, the cast would be illegal.[109] Furthermore, in the comparison bp == cp, cp is implicitly converted to a B*, since that is the only way to make the comparison meaningful in general (that is, upcasting is always allowed), hence the true result. So when converting back and forth between subobjects and complete types, the appropriate offset is applied.
The null pointer requires special handling, obviously, since blindly subtracting an offset when converting to or from a B subobject will result in an invalid address if the pointer was zero to start with. For this reason, when casting to or from a B*, the compiler generates logic to check first to see if the pointer is zero. If it isn’t, it applies the offset; otherwise, it leaves it as zero.
With the syntax we’ve seen so far, if you have multiple base classes, and if those base classes in turn have a common base class, you will have two copies of the top-level base, as you can see in the following example.
//: C09:Duplicate.cpp
// Shows duplicate subobjects
#include <iostream>
using namespace std;
class Top {
int x;
public:
Top(int n) { x = n; }
};
class Left : public Top {
int y;
public:
Left(int m, int n) : Top(m) { y = n; }
};
class Right : public Top {
int z;
public:
Right(int m, int n) : Top(m) { z = n; }
};
class Bottom : public Left, public Right {
int w;
public:
Bottom(int i, int j, int k, int m)
: Left(i, k), Right(j, k) { w = m; }
};
int main() {
Bottom b(1, 2, 3, 4);
cout << sizeof b << endl; // 20
} ///:~
Since the size of b is 20 bytes,[110] there are five integers altogether in a complete Bottom object. A typical class diagram for this scenario usually appears as:
This is the so-called "diamond inheritance", but in this case it would be better rendered as:
The awkwardness of this design surfaces in the constructor for the Bottom class in the previous code. The user thinks that only four integers are required, but which arguments should be passed to the two parameters that Left and Right require? Although this design is not inherently "wrong," it is usually not what an application calls for. It also presents a problem when trying to convert a pointer to a Bottom object to a pointer to Top. As we showed earlier, the address may need to be adjusted, depending on where the subobject resides within the complete object, but in this case there are two Top subobjects to choose from. The compiler doesn’t know which to choose, so such an upcast is ambiguous and therefore not allowed. The same reasoning explains why a Bottom object would not be able to call a function that is only defined in Top. If such a function Top::f( ) existed, calling b.f( ) above would need to refer to a Top subobject as a context in which to execute, and there are two to choose between.
Virtual base classes
What we usually want in such cases is true diamond inheritance, in which a single Top object is shared by both Left and Right subobjects within a complete Bottom object, which is what the first class diagram depicts. This is achieved by making Top a virtual base class of Left and Right:
//: C09:VirtualBase.cpp
// Shows a shared subobject via a virtual base
#include <iostream>
using namespace std;
class Top {
protected:
int x;
public:
Top(int n) { x = n; }
virtual ~Top(){}
friend ostream&
operator<<(ostream& os, const Top& t) {
return os << t.x;
}
};
class Left : virtual public Top {
protected:
int y;
public:
Left(int m, int n) : Top(m) { y = n; }
};
class Right : virtual public Top {
protected:
int z;
public:
Right(int m, int n) : Top(m) { z = n; }
};
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 << b.x << ',' << b.y << ',' << b.z
<< ',' << b.w;
}
};
int main() {
Bottom b(1, 2, 3, 4);
cout << sizeof b << endl;
cout << b << endl;
cout << static_cast<void*>(&b) << endl;
Top* p = static_cast<Top*>(&b);
cout << *p << endl;
cout << static_cast<void*>(p) << endl;
cout << dynamic_cast<void*>(p) << endl;
} ///:~
109
But not detected as an error.В dynamic_cast, however can solve this problem. See the next chapter for details.
110
Compilers can add arbitrary padding, so the size of an object must be at least as large as the sum of its parts, but can be larger.