Most of the time you should let virtual functions do that job for you, but when writing special-purpose object-software tools, such as debuggers, database viewers, or class browsers, you
Trang 157 Create a valarray<int> with 12 random values Create another valarray<int>
with 20 random values You will interpret the first valarray as a 3 x 4 matrix of ints and the second as a 4 x 5 matrix of ints, and multiply them by the rules of matrix multiplication Store the result in a valarray<int> of size 15, representing the 3 x 5
result matrix Use slices to multiply the rows of the first matrix time the columns of the second Print the result in rectangular matrix form
Special Topics
The mark of a professional in any field appears in his or her attention
to the finer points of the craft In this part of the book we discuss
advanced features of C++ along with development techniques used by polished C++ professionals.
Once in a great while you may need to depart from the conventional wisdom of sound oriented design by inspecting the runtime type of an object for special processing Most of the time you should let virtual functions do that job for you, but when writing special-purpose
object-software tools, such as debuggers, database viewers, or class browsers, you’ll need to determine type information at runtime This is where the runtime type identification (RTTI) mechanism comes into play, which is the topic of Chapter 8
Multiple inheritance has taken a bad rap over the years, and some languages don’t even support it Nonetheless, when used properly, it can be a powerful tool for crafting elegant, efficient code A number of standard practices involving multiple inheritance have evolved over the years, which
we present in Chapter 9
Perhaps the most notable innovation in software development since object-oriented techniques is the use of design patterns A design pattern describes and presents solutions for many of the common problems involved in designing software, and can be applied in many situations and implemented in any language In chapter 10 we describe a selected number of widely-used design patterns and implement them in C++
Chapter 11 explains in detail the benefits and challenges of multi-threaded programming The current version of standard C++ does not specify support for threads, even though most operating systems support them We use a portable, freely-available thread library to illustrate how C++ programmers can take advantage of threads to build more usable and responsive applications
8: Runtime type identification
Runtime type identification (RTTI) lets you find the dynamic type of
an object when you have only a pointer or a reference to the base type.
This can be thought of as a “secondary” feature in C++, pragmatism to help out when you get into rare messy situations Normally, you’ll want to intentionally ignore the exact type of an object and let the virtual function mechanism implement the correct behavior for that type automatically On
occasion, however, it’s useful to know the exact runtime (that is, most derived) type of an object
for which you only have a base pointer Often this information allows you to perform a
Part 3
Trang 2case operation more efficiently or prevent a base-class interface from becoming ungainly It happens enough that most class libraries contain virtual functions to produce run-time type information When exception handling was added to C++, it required information about the runtime type of objects It became an easy next step to build access to that information into the language This chapter explains what RTTI is for and how to use it Comment
Runtime casts
One way to determine the runtime type of an object through a pointer is to employ a runtime cast,
which verifies that the attempted conversion is valid This is useful when you need to cast a class pointer to a derived type Since inheritance hierarchies are typically depicted with base
base-classes above derived base-classes, such a cast is called a downcast.
Consider the following class hierarchy
In the code that follows, the Investment class has an extra operation that the other classes do not, so it is important to be able to know at runtime whether a Security pointer refers to a
Investment object or not To implement checked runtime casts, each class keeps an integral
identifier to distinguish it from other classes in the hierarchy Comment
virtual bool isA(int id) {
return (id == BASEID);
}
};
class Stock : public Security {
typedef Security Super;
Trang 3return id == TYPEID || Super::isA(id);
class Bond : public Security {
typedef Security Super;
protected:
enum {OFFSET = 2, TYPEID = BASEID + OFFSET};public:
bool isA(int id) {
return id == TYPEID || Super::isA(id);
class Investment : public Security {
typedef Security Super;
protected:
enum {OFFSET = 3, TYPEID = BASEID + OFFSET};public:
bool isA(int id) {
return id == BASEID || Super::isA(id);
class Metal : public Investment {
typedef Investment Super;
protected:
enum {OFFSET = 4, TYPEID = BASEID + OFFSET};public:
bool isA(int id) {
return id == BASEID || Super::isA(id);
Trang 4cout << "cast from intermediate pointer:\n";
Security* sp = new Metal;
The polymorphic isA( ) function checks to see if its argument is compatible with its type
argument (id), which means that either id matches the object’s typeID exactly or that of one of its ancestors in the hierarchy (hence the call to Super::isA( ) in that case) The dynacast( ) function, which is static in each class, calls isA( ) for its pointer argument to check if the cast is valid If isA( ) returns true, the cast is valid, and a suitably cast pointer is returned Otherwise,
the null pointer is returned, which tells the caller that the cast is not valid, meaning that the original pointer is not pointing to an object compatible with (convertible to) the desired type All
this machinery is necessary to be able to check intermediate casts, such as from a Security pointer that refers to a Metal object to a Investment pointer in the previous example program.
Comment
Although for most programs downcasting is not needed (and indeed is discouraged, since
everyday polymorphism solves most problems in object-oriented application programs), the ability to check a cast to a more derived type is important for utility programs such as debuggers,
class browsers, and databases C++ provides such a checked cast with the dynamic_cast operator The following program is a rewrite of the previous example using dynamic_cast
class Stock : public Security {};
class Bond : public Security {};
class Investment : public Security {
Trang 5cout << "cast from intermediate pointer:\n";
Security* sp = new Metal;
This example is much shorter, since most of the code in the original example was just the
overhead for checking the casts The target type of a dynamic_cast is placed in angle brackets, like the other new-style C++ casts (static_cast, and so on), and the object to cast appears as the
operand dynamic_cast requires that the types you use it with be polymorphic if you want safe
downcasts This in turn requires that the class must have at least one virtual function
Fortunately, the Security base class has a virtual destructor, so we didn’t have to invent some extraneous function to get the job done dynamic_cast does its work at runtime, of course, since
it has to check the virtual function table of objects according to there dynamic type This naturally
implies that dynamic_cast tends to be more expensive than the other new-style casts CommentYou can also use dynamic_cast with references instead of pointers, but since there is no such
thing as a null reference, you need another way to know if the cast fails That “other way” is to
catch a bad_cast exception, as follows:
The bad_cast class is defined in the <typeinfo> header, and, like most of the standard library,
is declared in the std namespace Comment
The typeid operator
The other way to get runtime information for an object is through the typeid operator This operator returns an object of class type_info, which yields information about the type of object
to which it was applied If the type is polymorphic, it gives information about the most derived
type that applies (the dynamic type); otherwise it yields static type information One use of the
typeid operator is to get the name of the dynamic type of an object as a const char*, as you can
see in the following example Comment
Trang 6struct PolyBase {virtual ~PolyBase(){}};
struct PolyDer : PolyBase {};
const PolyBase* ppb = &pd;
cout << typeid(ppb).name() << endl;
cout << typeid(*ppb).name() << endl;
cout << boolalpha << (typeid(*ppb) == typeid(pd))
const NonPolyBase* nppb = &npd;
cout << typeid(nppb).name() << endl;
cout << typeid(*nppb).name() << endl;
cout << (typeid(*nppb) == typeid(npd))
The output from this program is
struct PolyBase const *
The first output line just echoes the static type of ppb because it is a pointer To get RTTI to kick
in, you need to look at the object a pointer or reference is connected to, which is illustrated in the
second line Notice that RTTI ignores top-level const and volatile qualifiers With
non-polymorphic types, you just get the static type (the type of the pointer itself) As you can see,
built-in types are also supported Comment
It turns out that you can’t store the result of a typeid operation in a type_info object, because
there are no accessible constructors and assignment is disallowed; you must use it as we have
shown In addition, the actual string returned by type_info::name( ) is compiler dependent Some compilers return “class C” instead of just “C”, for instance, for a class named C Applying
typeid to an expression that dereferences a null pointer will cause a bad_typeid exception (also
defined in <typeinfo>) to be thrown Comment
The following example shows that the class name that type_info::name( ) returns is fully
Trang 7Since Nested is a member type of the One class, the result is One::Nested Comment
You can also ask a type_info object if it precedes another type_info object in the
implementation-defined “collation sequence” (the native ordering rules for text), using before
(type_info&), which returns true or false When you say, Comment
if(typeid(me).before(typeid(you))) //
you’re asking if me occurs before you in the current collation sequence This is useful should you use type_info objects as keys Comment
Casting to intermediate levels
As you saw in the earlier program that used the hierarchy of Security classes, dynamic_cast
can detect both exact types and, in an inheritance hierarchy with multiple levels, intermediate types Here is another example
class MI : public B1, public B2 {};
class Mi2 : public MI {};
This example has the extra complication of multiple inheritance (more on this later in this
chapter) If you create an Mi2 and upcast it to the root (in this case, one of the two possible roots
Trang 8is chosen), the dynamic_cast back to either of the derived levels MI or Mi2 is successful
Casting to intermediate levels brings up an interesting difference between dynamic_cast and
typeid The typeid operator always produces a reference to a static typeinfo object that
describes the dynamic type of the object Thus, it doesn’t give you intermediate-level information
In the following expression (which is true), typeid doesn’t see b2 as a pointer to the derived type, like dynamic_cast does:
typeid(b2) != typeid(Mi2*)
The type of b2 is simply the exact type of the pointer:
typeid(b2) == typeid(B2*)
void pointers
RTTI only works for complete types, meaning that all class information must be available when
typeid is used In particular, it doesn’t work with void pointers:
virtual void happy() {}
virtual void joy() {}
A void* truly means “no type information at all.” Comment
Using RTTI with templates
Class templates work well with RTTI, since all they do is generate classes As usual, RTTI provides
a convenient way to obtain the name of the class you’re in The following example prints the order
of constructor and destructor calls: Comment
Trang 9This template uses a constant int to differentiate one class from another, but type arguments
would work as well Inside both the constructor and destructor, RTTI information produces the
name of the class to print The class X uses both inheritance and composition to create a class that
has an interesting order of constructor and destructor calls The output is: Comment
Of course, the RTTI mechanisms must work properly with all the complexities of multiple
inheritance, including virtual base classes (discussed in depth in the next chapter—you may want
to come back to this after reading Chapter 9):
class B1 : virtual public BB {};
class B2 : virtual public BB {};
class MI : public B1, public B2 {};
int main() {
Trang 10BB* bbp = new MI; // Upcast
// Proper name detection:
cout << typeid(*bbp).name() << endl;
// Dynamic_cast works properly:
MI* mip = dynamic_cast<MI*>(bbp);
// Can't force old-style cast:
//! MI* mip2 = (MI*)bbp; // Compile error
} ///:~
The typeid( ) operation properly detects the name of the actual object, even through the virtual base class pointer The dynamic_cast also works correctly But the compiler won’t even allow
you to try to force a cast the old way: Comment
MI* mip = (MI*)bbp; // Compile-time error
The compiler knows this is never the right thing to do, so it requires that you use a
dynamic_cast Comment
Sensible uses for RTTI
Because it allows you to discover type information from an anonymous polymorphic pointer, RTTI is ripe for misuse by the novice because RTTI may make sense before virtual functions do For many people coming from a procedural background, it’s difficult not to organize programs
into sets of switch statements They could accomplish this with RTTI and thus lose the important
value of polymorphism in code development and maintenance The intent of C++ is that you use virtual functions throughout your code and that you only use RTTI when you must CommentHowever, using virtual functions as they are intended requires that you have control of the base-class definition because at some point in the extension of your program you may discover the base class doesn’t include the virtual function you need If the base class comes from a library or is otherwise controlled by someone else, a solution to the problem is RTTI: you can derive a new type and add your extra member function Elsewhere in the code you can detect your particular type and call that member function This doesn’t destroy the polymorphism and extensibility of the program, because adding a new type will not require you to hunt for switch statements
However, when you add new code in the main body that requires your new feature, you’ll have to detect your particular type Comment
Putting a feature in a base class might mean that, for the benefit of one particular class, all the other classes derived from that base require some meaningless stub for a pure virtual function This makes the interface less clear and annoys those who must redefine pure virtual functions when they derive from that base class.Comment
Finally, RTTI will sometimes solve efficiency problems If your code uses polymorphism in a nice way, but it turns out that one of your objects reacts to this general-purpose code in a horribly inefficient way, you can pick that type out using RTTI and write case-specific code to improve the efficiency Comment
A trash recycler
To further illustrate a practical use of RTTI, the following program simulates a trash recycler Different kinds of “trash” are inserted into a single container and then later sorted according to their dynamic types Comment
//: C08:Recycle.cpp
// A Trash Recycler
#include <cstdlib>
#include <ctime>
Trang 11virtual float value() const = 0;
float weight() const { return _weight; } virtual ~Trash() { cout << "~Trash()\n"; }};
class Aluminum : public Trash {
static float val;
public:
Aluminum(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
val = newval;
}
};
float Aluminum::val = 1.67;
class Paper : public Trash {
static float val;
public:
Paper(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
val = newval;
}
};
float Paper::val = 0.10;
class Glass : public Trash {
static float val;
public:
Glass(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
}
os << "Total value = " << val << endl;
}
Trang 12vector<Trash*>::iterator sorter = bin.begin();
// Sort the Trash:
else if(pp) paperBin.push_back(pp);
else if(gp) glassBin.push_back(gp);
We can do even better by using a map that associates pointers to type_info objects with a vector
of Trash pointers Since a map requires an ordering predicate, we provide one named
TInfoLess that calls type_info::before( ) As we insert Trash pointers into the map, they are
associated automatically with their type_info key Comment
Trang 13virtual float value() const = 0;
float weight() const { return wt; }
virtual ~Trash() { cout << "~Trash()\n"; }
};
class Aluminum : public Trash {
static float val;
public:
Aluminum(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
val = newval;
}
};
float Aluminum::val = 1.67;
class Paper : public Trash {
static float val;
public:
Paper(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
val = newval;
}
};
float Paper::val = 0.10;
class Glass : public Trash {
static float val;
public:
Glass(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
// Sums up the value of the Trash in a bin:
void sumValue(const TrashMap::value_type& p, ostream& os) { vector<Trash*>::const_iterator tally = p.second.begin(); float val = 0;
Trang 14We’ve modified sumValue( ) to call type_info::name( ) directly, since the type_info object
is now available there as the first member of the TrashMap::value_type pair This avoids the extra call to typeid to get the name of the type of Trash being processed that was necessary in
the previous version of this program Comment
Mechanism and overhead of RTTI
Typically, RTTI is implemented by placing an additional pointer in a class’s virtual function table
This pointer points to the type_info structure for that particular type The effect of a typeid( ) expression is quite simple: the virtual function table pointer fetches the type_info pointer, and a reference to the resulting type_info structure is produced Since this is just a two-pointer
dereference operation, it is a constant time operation
For a dynamic_cast<destination*>(source_pointer), most cases are quite straightforward:
source_pointer’s RTTI information is retrieved, and RTTI information for the type
destination* is fetched A library routine then determines whether source_pointer’s type is of
type destination* or a base class of destination* The pointer it returns may be adjusted
because of multiple inheritance if the base type isn’t the first base of the derived class The
situation is (of course) more complicated with multiple inheritance in which a base type may appear more than once in an inheritance hierarchy and virtual base classes are used
Because the library routine used for dynamic_cast must check through a list of base classes, the overhead for dynamic_cast may be higher than typeid( ) (but of course you get different
information, which may be essential to your solution), and it may take more time to discover a
base class than a derived class In addition, dynamic_cast allows you to compare any type to
any other type; you aren’t restricted to comparing types within the same hierarchy This adds
extra overhead to the library routine used by dynamic_cast.
Trang 15Although normally you upcast a pointer to a base class and then use the generic interface of that base class (via virtual functions), occasionally you get into a corner where things can be more effective if you know the dynamic type of the object pointed to by a base pointer, and that’s what RTTI provides The most common misuse may come from the programmer who doesn’t
understand virtual functions and uses RTTI to do type-check coding instead The philosophy of C++ seems to be to provide you with powerful tools and guard for type violations and integrity, but if you want to deliberately misuse or get around a language feature, there’s nothing to stop you Sometimes a slight burn is the fastest way to gain experience
Exercises
1 Modify C16:AutoCounter.h in Volume 1 of this series so that it becomes a useful
debugging tool It will be used as a nested member of each class that you are
interested in tracing Turn AutoCounter into a template that takes the class name of
the surrounding class as the template argument, and in all the error messages use RTTI to print out the name of the class
58 Use RTTI to assist in program debugging by printing out the exact name of a
template using typeid( ) Instantiate the template for various types and see what the
results are
59 Modify the Instrument hierarchy from Chapter 14 of Volume 1 by first copying
Wind5.cpp to a new location Now add a virtual ClearSpitValve( ) function to the Wind class, and redefine it for all the classes inherited from Wind Instantiate a TStash to hold Instrument pointers, and fill it with various types of Instrument
objects created using the new operator Now use RTTI to move through the container looking for objects in class Wind, or derived from Wind Call the ClearSpitValve( )
function for these objects Notice that it would unpleasantly confuse the Instrument
base class if it contained a ClearSpitValve( ) function Comment
9: Multiple inheritance
The basic concept of multiple inheritance (MI) sounds simple
enough: you create a new type by inheriting from more than one base class The syntax is exactly what you’d expect, and as long as the inheritance diagrams are simple, MI can be simple as well.
Or maybe not! MI can introduce a number of ambiguities and strange situations, which are covered in this chapter But first, it will be helpful to get a little perspective on the subject
Comment
Perspective
Before C++, the most successful object-oriented language was Smalltalk Smalltalk was created
from the ground up as an object-oriented language It is often referred to as pure, whereas C++ is called a hybrid language because it supports multiple programming paradigms, not just the
object-oriented paradigm One of the design decisions made with Smalltalk was that all classes
would be derived in a single hierarchy, rooted in a single base class (called Object—this is the
model for the object-based hierarchy) You cannot create a new class in Smalltalk without
deriving it from an existing class, which is why it takes a certain amount of time to become
productive in Smalltalk: you must learn the class library before you can start making new classes The Smalltalk class hierarchy is therefore a single monolithic tree
Trang 16Classes in Smalltalk usually have a number of things in common, and they always have some
things in common (the characteristics and behaviors of Object), so you almost never run into a
situation in which you need to inherit from more than one base class However, with C++ you can create as many hierarchy trees as you want Therefore, for logical completeness the language must
be able to combine more than one class at a time—thus the need for multiple inheritance
It was not a crystal clear, however, that programmers could not get by without multiple
inheritance, and there was (and still is) a lot of disagreement about whether it is really essential in
C++ MI was added in AT&T cfront release 2.0 and was the first significant change to the
language Since then, a number of other features have been added (notably templates and
exceptions) that change the way we think about programming and place MI in a much less
important role You can think of MI as a “minor” language feature that is seldom involved in your daily design decisions
One of the most pressing issues at the time that drove MI involved containers Suppose you want
to create a container that everyone can easily use One approach is to use void* as the type inside the container The Smalltalk approach, however, is to make a container that holds Objects (Remember that Object is the base type of the entire Smalltalk hierarchy.) Because everything in Smalltalk is ultimately derived from Object, any container that holds Objects can hold anything
Comment
Now consider the situation in C++ Suppose vendor A creates an object-based hierarchy that includes a useful set of containers including one you want to use called Holder Now you come across vendor B’s class hierarchy that contains some other class that is important to you, a
BitImage class, for example, that holds graphic images The only way to make a Holder of BitImages is to derive a new class from both Object, so it can be held in the Holder, and BitImage:
Comment
This was seen as an important reason for MI, and a number of class libraries were built on this model However, as you saw in Chapter 5, the addition of templates has changed the way
containers are created, so this situation isn’t a driving issue for MI
The other reason you may need MI is related to design You can intentionally use MI to make a design more flexible or useful (or at least seemingly so) An example of this is in the original
iostream library design (which still persists in today’s template design, as you saw in Chapter 4):
Trang 17a derived class object, but this doesn’t affect the derived class; it still contains all base class data and can access all non-private base class members Comment
Interface inheritance, on the other hand, only adds member function declarations to a derived class interface and is not directly supported in C++ The usual technique to simulate interface
inheritance in C++ is to derive from an interface class, which is a class that contains only
declarations (no data or function bodies) These declarations will be pure virtual functions, of course Here is an example Comment
Trang 18void testStringable(const Stringable& s) {
string buf = s.toString() + "th";
cout << buf << endl;
print( ) The test functions have no need to know the most-derived type of their parameter; they
just need an object that is substitutable for their parameter’s type Comment
As usual, a template solution is more compact:
Trang 19void testStringable(const Stringable& s) {
string buf = s.toString() + "th";
cout << buf << endl;
The names Printable, Intable, and Stringable are now just template parameters that assume
the existence of the operations indicated in their respective contexts Some people are more comfortable with the first version, because the type names guarantee by inheritance that the expected interfaces are implemented Others are content with the fact that if the operations required by the test functions are not satisfied by their template type arguments, the error is still caught at compile time The latter approach is technically a “weaker” form of type checking than the former (inheritance) approach, but the effect on the programmer (and the program) is the same This is one form of weak typing that is acceptable to many of today’s C++ programmers
Trang 20which are classes not intended to be instantiated independently, but exist to add capabilities to other classes through inheritance Comment
As an example, suppose we are clients of a class that supports access to a database We will likely only have a header file available (which is part of the point we are about to make), but for
illustration, assume the following, simple implementation of a Database class: Comment
struct DatabaseError : runtime_error {
DatabaseError(const string& msg) : runtime_error(msg)
void open() throw(DatabaseError) {
cout << "connected to " << dbid << '\n';
of client entities using the database connection and to automatically terminate the connection
Trang 21when that count goes to zero To add reference counting to the Database class, we create a mixin class named Countable and mix it into the Database class by creating a new class,
DBConnection, through multiple inheritance Here’s the Countable mixin class: Comment
It is evident that this is not a standalone class because its constructor is protected; it therefore
requires a friend or a derived class to use it It is important that the destructor is virtual, of course,
because it is called only from the delete this statement in detach( ), and we of course want
derived objects to be completely destroyed The DBConnection class derives from both
Database and Countable and provides a static create( ) function that initializes its
Countable subobject (This is an example of the Factory Method design pattern, discussed in the
next chapter.) Comment
Trang 22We now have a reference-counted database connection without modifying the Database class,
and we can safely assume that it will not be surreptitiously terminated The opening and closing is done using the Resource Acquisition Is Initialization idiom (RAII) mentioned in Chapter 1 via the
DBConnection constructor and destructor This makes using a DBConnection easy to use, as
the following program shows Comment
uses RAII to manage its use of the connection When the program terminates, the destructors for
the two DBClient objects will decrement the reference count (by calling detach( ), which
DBConnection inherited from Countable), and the database connection will be closed when
the count reaches zero after the object c1 is destroyed (This is because of Countable’s virtual
destructor, as we explained earlier.) Comment
A template approach is commonly used for mixin inheritance, allowing the user to specify at compile time which flavor of mixin is desired This way you can use different reference-counting
approaches without explicitly defining DBConnection twice Here’s how it’s done Comment
//: C09:DBConnection2.h
// A parameterized mixin
Trang 23The only change here is the template prefix to the class definition (and renaming Countable to
Counter for clarity) We could also make the database class a template parameter (had we
multiple database access classes to choose from), but it is not a mixin, per se, since it is a
standalone class The following example uses the original Countable as the Counter mixin type, but we could use any type that implements the appropriate interface (attach( ), detach( ), and
Trang 24The general pattern for multiple parameterized mixins is simply:
template<class Mixin1, class Mixin2, … , class MixinK>
class Subject : public Mixin1,
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;
Trang 25As 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) Comment
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 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 Comment
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 CommentWith 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 Comment
Trang 26Since 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:
Comment
[110]