Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ Timesaving Techniques (2005) [eng].pdf
Скачиваний:
65
Добавлен:
16.08.2013
Размер:
8.35 Mб
Скачать

Testing the Complete Class 113

int getX(void) const

{

return x;

}

double getY(void) const

{

return y;

}

std::string getS(void) const

{

return s;

}

//Note: For internal pointers, always return a CONST pointer. const char * getP(void) const

{

return p;

}

//Implement a clone operator.

Complete Clone(void)

{

Complete c; c.Copy( *this ); return c;

}

// Implement an assignment operator. Complete operator=( const Complete& aCopy )

{

Copy( aCopy ); return *this;

}

};

3. Save the source file.

When you are creating your own classes, you should seriously consider using Listing 21-1 as a template. If all classes implemented all their functionality in this manner, they would easily rid the programming world of 10 to 20 percent of all bugs.

developers. First, they provide a way to make sure that you didn’t make the code work improperly when you made the change. Secondly, and more importantly, they act as a tutorial for the user of your class. By running your test driver and looking at the order in which you do things, the programmer can discover how to use the code in his or her own program as well as what values are expected and which are error values. It is almost, but not quite, self-documenting code.

Testing the Complete Class

Just writing the class is not enough. You also need to test it. Test cases provide two different uses for

How do you go about writing tests for your classes? Well, first you test all the “normal” conditions. One quick way to illustrate is to create a test driver that tests the constructors for the class, like this:

114 Technique 21: Creating a Complete Class

1. In the code editor of your choice, reopen

the existing file to hold the code for your test program.

In this example, I named the test program ch21.cpp.

2. Type the code from Listing 21-2 into your file.

Better yet, copy the code from the source file on this book’s companion Web site.

LISTING 21-2: THE COMPLETE CLASS TEST DRIVER

void DumpComplete( const Complete& anObj )

{

printf(“Object:\n”);

printf(“X : %d\n”, anObj.getX() ); printf(“Y : %lf\n”, anObj.getY() ); printf(“S : %s\n”, anObj.getS().c_str() ); printf(“P : %s\n”, anObj.getP() ? anObj.getP() : “NULL” );

}

int main()

{

//Test the void constructor. Complete c1;

//Test the full constructor. std::string s = “Three”;

Complete c2(1, 2.0, s, “This is a test”);

//Test the copy constructor.

Complete c3(c2);

// Test the = operator. Complete c4=c1;

DumpComplete( c1 ); DumpComplete( c2 ); DumpComplete( c2 ); DumpComplete( c4 );

}

3. Save the source-code file in your editor and close the code-editor application.

4. Compile the application, using your favorite compiler on your favorite operating system.

5. Run the application. You should see the output from Listing 21-3 appear in the console window.

LISTING 21-3: OUTPUT

$ ./a.exe Object: X : 0

Y : 0.000000 S :

P : NULL

Object:

X : 1

Y : 2.000000 S : Three

P : This is a test Object:

X : 1

Y : 2.000000 S : Three

P : This is a test Object:

X : 0

Y : 0.000000 S :

P : NULL

As you can see, we get the expected output. The initialized values default to what we set them to in the various constructors and copy functions. It’s very hard to overestimate the importance of having unit tests like these. With unit tests, you have a built-in way to verify changes; you have ways to start testing your software; and you have documentation built right in the code. Unit testing is an important portion of such development methodologies as eXtreme Programming (now often called Agile Programming) and the like.

Another important thing to note in our code is the dirty flag (shown at 1 in Listing 21-1) that we have built into the code. The simple dirty flag could easily be extracted out into its own small class to manage dirty objects — and can be reused across many different classes, as shown in Listing 21-4.

Testing the Complete Class 115

LISTING 21-4: A CHANGEMANAGEMENT CLASS

class ChangeManagement

{

private:

bool dirty; public:

ChangeManagement(void)

{

dirty = false;

}

ChangeManagement( bool flag )

{

setDirty(flag);

}

ChangeManagement( const ChangeManagement& aCopy )

{

setDirty(aCopy.isDirty());

}

virtual ~ChangeManagement()

{

}

void setDirty( bool flag )

{

dirty = flag;

}

bool isDirty(void)

{

return dirty;

}

ChangeManagement& operator=(const ChangeManagement& aCopy)

{

setDirty( aCopy.isDirty() ); return *this;

}

};

Okay, why might we want to create a dirty flag? Well, for one reason, we could then manage the “dirtiness” of objects outside the objects themselves. We might, for example, have a manager that the ChangeManagement class “talked to” to notify it when given objects become dirty or clean (and therefore have to be written to and from disk). This sort of thing would be very useful for a cache manager

of objects, when memory is at a premium but disk or other storage space is available.

Of course, writing unit tests doesn’t mean that you have done a complete test. At this point, you’ve simply validated that the code does what you expected it to do when valid input was given. Never confuse testing with validation. Validation shows that the code does what it is supposed to do when you do everything right. Testing shows that the code does what it is supposed to do when you do something wrong.

22 Using Virtual

Inheritance

Technique

Save Time By

Understanding inheritance

Defining virtual inheritance

Implementing virtual inheritance

Testing and correcting your code

It’s no accident that this book devotes some time to understanding how classes are constructed — and how you can inherit from them. Inheritance allows you to save time and effort by providing a ready

base of code that can be reused in your own classes. When you are doing class design, however, there are some problems that will cause you pain and consternation. One such problem is in multiple inheritance. While multiple inheritance can solve many problems in class design, it can also cause problems. For example, consider the case of Listing 22-1:

LISTING 22-1: A MULTIPLE INHERITANCE EXAMPLE

class Base

{

char *name; public:

virtual const char *Name(); void setName( const char *n );

}

class A : public Base

{

}

class B : public Base

{

}

class C : public A, public B

{

}

Listing 22-1 shows a problem with multiple inheritance that you might not think could ever happen — but it happens nearly all the time. When we have a base class from which all classes in the system inherit information — such as an Object class that stores the name (that is, the

type) of the class — we run into the problem of inheriting from the same base class in multiple ways. This situation is often referred to as the “deadly triangle” of object-oriented programming. If you instantiate an object of type C, as in the code shown in Listing 22-1, the compiler and

Using Virtual Inheritance 117

linker won’t object — everything appears to work fine. However, the problem comes in when we try to use the methods in the base class Base. If we write

const char *name = c.Name();

then the compiler immediately throws a fit, generating an error for the source-code line. The reason should be obvious: Which name method are we calling here? There are two Base classes in the inheritance tree for class C. Is it the one in the base class A, or the one in the base class B? Neither is obvious, although we can scope the answer with a little effort:

const char *name = c.B::Name();

This will fix our compile problem, because the compiler now knows that we mean the Name method that is inherited through the class B tree.

Unfortunately, this doesn’t really solve the problem. After all, our C class is not a B, nor is it an A — it is a C, and only that. You might think a dodge like this could “fix” the problem:

class C : public A, public B

{

public:

C()

: Base(“C”)

{

}

Here we explicitly tell the compiler that this is a C object and should be used as one. Because the base class constructor takes a name, and that name is used in the Name method, it ought to therefore assign the value C to the name. The problem is, if you try to compile this mess, you get the error shown in Listing 22-2:

Curses, foiled again; this doesn’t solve the problem at all. The compiler is complaining because it does not see which base class we are trying to use. Is it the base class of A, or the base class of B? This isn’t clear, and therefore we cannot simply call the base class constructor.

How, then, can we inherit from two base classes that (in turn) inherit from a common base class? The answer lies in a C++ concept known as virtual inheritance. Virtual inheritance combines multiple base classes into a single base class within the inheritance tree, so that there is never any ambiguity.

In Listing 22-1, with two classes inheriting from a common base, the problem occurs because the base class occurs in the inheritance tree twice. Virtual inheritance forces the compiler to create only a single instance of the base class — and to use the data in that single instance wherever the data was last set. This means that the code literally skips the instantiation of the base classes in the two bases

(A and B, in our example) and uses only the instantiation in the last class level, C.

LISTING 22-2: COMPILER OUTPUT FOR MULTIPLE INHERITANCE ERROR

ch3_11.cpp: In

constructor ’C::C()’:

 

 

ch3_11.cpp:65:

error: ’Object’ is

an ambiguous base

of ’C’

 

ch3_11.cpp:65:

error: type ’class

Object’ is not a direct

base of ’C’

ch3_11.cpp: In

member function ’void C::Display()’:

 

 

ch3_11.cpp:70:

error: request for

member ’Name’ is ambiguous in multiple

inheritance

lattice

 

 

 

ch3_11.cpp:23:

error: candidates are: virtual const

char*

Object::Name()

ch3_11.cpp:23:

error:

virtual const

char*

Object::Name()