1. Trang chủ
  2. » Công Nghệ Thông Tin

thinking in c 2nd ed volume 2 rev 20 - phần 8 pps

52 264 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 52
Dung lượng 124,46 KB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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 1

57 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 2

case 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 3

return 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 4

cout << "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 5

cout << "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 6

struct 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 7

Since 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 8

is 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 9

This 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 10

BB* 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 11

virtual 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 12

vector<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 13

virtual 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 14

We’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 15

Although 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 16

Classes 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 17

a 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 18

void 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 19

void 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 20

which 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 21

when 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 22

We 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 23

The 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 24

The 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 25

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) 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 26

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:

Comment

[110]

Ngày đăng: 13/08/2014, 09:20

TỪ KHÓA LIÊN QUAN