Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
When you inherit from a base class, you get a copy of all the data members of that base class in your derived class. The following
program shows how multiple base subobjects might be laid out in memory:
//: C09:Offset.cpp
// Illustrates layout of subobjects with MI.
#include <iostream>
using namespace std;
class A { int x; };
class B { int y; };
class C : public A, public B { int z; };
int main() {
cout << "sizeof(A) == " <<
sizeof(A) << endl;
cout << "sizeof(B) == " <<
sizeof(B) << endl;
cout << "sizeof(C) == " <<
sizeof(C) << endl;
C c;
cout << "&c == " << &c
<< endl;
A* ap = &c;
B* bp = &c;
cout << "ap == " <<
static_cast<void*>(ap) << endl;
cout << "bp == " <<
static_cast<void*>(bp) << endl;
C* cp = static_cast<C*>(bp);
cout << "cp == " <<
static_cast<void*>(cp) << endl;
cout << "bp == cp? " <<
boolalpha << (bp == cp) << endl;
cp = 0;
bp = cp;
cout << bp << endl;
}
/* Output:
sizeof(A) == 4
sizeof(B) == 4
sizeof(C) == 12
&c == 1245052
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).
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. Furthermore,
in the comparison bp == cp, cp is implicitly converted to a B*,
since that is the only way to make the comparison meaningful (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, 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 needs. 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 here there are two Top subobjects to choose from. The
compiler doesn t know which to choose, so such an upcast is ambiguous and is
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 an execution context, and there are
two to choose from.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |