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

392 Technique 63: Creating Debugging Macros and Classes

The above code listing is a simple program that accepts a certain number of input arguments, prints them out, and then prompts the user for some commands. It could do just about anything with either the input arguments or the commands, but that really isn’t important for our purposes. Just to modify the string for output, we reverse the characters in the output string. We log the input arguments, as well as the input string and output strings in the command loop. This indicates any potential problems with specific strings.

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

4. Compile the source file with your favorite compiler on your favorite operating system.

5. Run the application.

If you have done everything properly, you should see the following output on your command console:

$ ./a -log

Enter command: Hello world

Enter command: Goodbye cruel world Enter command: Hell o again

Enter command:

$ cat log.txt

Program Startup Arguments

Input Argument 0 = ./a

Input Argument 1 = -log

Input String: Hello world

Output String: dlrow olleH

Input String: Goodbye cruel world

Output String: dlrow leurc eybdooG

Input String: Hell o again

Output String: niaga o lleH

Input String:

Finished with application

As you can see from the above listing, the log file contains all of the arguments, as well as the input and output strings from our session with the command prompts. The input strings are what we typed into the program, and the output strings are the expected reversed text.

As you can see, the application properly logged everything that was coming in and going out. Now, if we had a problem with the code (such as an occasional too-short output string), we could consult the log to look at what the user entered, and figure out why it didn’t work in the debugger.

Design by Contract

The final technique that we will look at for use in the debugging of C++ applications is actually one that you build in at the initial coding time, although you can add it after the fact. This technique — called Design by Contract — was primarily created by the Eiffel programming language. In the Design by Contract (DBC) methodology, there are three important parts of any piece of code:

Preconditions: These are conditions you take for granted before you can proceed with a process. For example, if you are trying to read from a file, it is important that the file be open first. If you are reading bytes from the file, and it is open, then you must specify a positive number of bytes, because reading a negative number of bytes makes no sense and could therefore lead to problems. Specifying a positive number of bytes, therefore, is a precondition for the process. A precondition is simply an assumption that you have made while you are coding the process.

By documenting these assumptions in the code itself, you make life easier for the user of the code, as well as for the maintainer of the code.

Validity checks: These are simply “sanity checks” for your object. For example, your object might contain a value that represents a person’s age. If that age value is negative, bad things are going to happen when you try to compute that person’s year of birth. This should

Design by Contract 393

never happen, of course, but it could happen due to programming errors, memory overwrites, or persistent storage errors. By checking the validity of an object each time that you work with it, you insure that the system is always in a known state.

Post-conditions: These are conditions that must be logically true after your process has completed. For example, if you are setting the length of a buffer in an object, the post-condition ensures that the length is zero or a positive number. (Negative lengths make no sense and indicate

an immediate processing error.) Another good example: A post-condition can check an assumption that a file has been opened — and valid values read in — at the end of a given process. If the file is not open, or there are no valid values in the output list, you know that something you didn’t anticipate happened during processing. For this reason, you check these assumptions in the postcondition block of your process.

The purpose of Design by Contract is to eliminate the source of errors before they crop up, which streamlines the development process by making error checking easier and debugging less intrusive. This will save you a lot of time in the long-run by making your programs more robust up front.

Documentation is a valuable asset in understanding code, but it does nothing to fix problems encountered while running the code. If you document the assumptions that your code makes while you’re writing it, you not only save those assumptions in one place, but also remind yourself to make the code check for those assumptions at run-time.

So how does it all work? Most DBC (Design by Contract) process controls are implemented via C++ macros. The following steps show you how this could be implemented in your own application:

1. In the code editor of your choice, create a new file to hold the code for the source file of the technique.

In this example, the file is named ch63b.cpp, although you can use whatever you choose. This file will contain the class definition for your automation object.

2. Type the code in Listing 63-4 into your file.

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

LISTING 63-4: THE DESIGN BY CONTRACT EXAMPLE

#include <iostream> #include <stdlib.h> #include <string.h>

void abort_program(const char *file, long line , const char *expression)

{

printf(“File: %s Line: %ld Expression Failed: %s\n”, file, line, expression); exit(1);

}

class DBCObject

{

long _magicNo; public:

DBCObject( long magic )

{

_magicNo = magic;

}

(continued)

394 Technique 63: Creating Debugging Macros and Classes

LISTING 63-4 (continued)

#ifdef _DEBUG

virtual bool IsValid() const = 0; #endif

long Magic(void) const

{

return _magicNo;

}

void setMagicNo( long magic )

{

_magicNo = magic;

}

};

#ifdef _DEBUG

#define DBC_ASSERT(bool_expression) if (!(bool_expression)) abort_program(__FILE__, __LINE__, #bool_expression)

#define IS_VALID(obj) DBC_ASSERT((obj) != NULL && (obj)->IsValid()) #define REQUIRE(bool_expression) DBC_ASSERT(bool_expression) #define ENSURE(bool_expression) DBC_ASSERT(bool_expression)

#else

//When your code is built in release mode, the _DEBUG flag would not be defined, thus there will be no overhead

//in the final release from these checks.

#define DBC_ASSERT(ignore) ((void) 1) #define IS_VALID(ignore) ((void) 1) #define REQUIRE(ignore) ((void) 1) #define ENSURE(ignore) ((void) 1)

#endif

class MyClass : public DBCObject

{

private:

char *_buffer; int _bufLen;

protected: void Init()

{

_buffer=NULL; _bufLen = 0;

}

#ifdef _DEBUG

bool IsValid() const

{

// Condition: Buffer not null. if ( getBuffer() == NULL )

return false;

Design by Contract 395

//Condition: Length > 0. if ( getLength() <= 0 )

return false;

//Condition: magic number correct. if ( Magic() != 123456 )

return false;

//All conditions are correct, so it’s okay to continue. return true;

}

#endif

public:

MyClass(void)

: DBCObject( 123456 )

{

Init();

}

MyClass( const char *strIn ) : DBCObject( 123456 )

{

// Precondition: strIn not NULL. REQUIRE( strIn != NULL );

Init();

setBuffer( strIn );

//Post-condition: buffer not NULL. ENSURE( getBuffer() != NULL );

//Post-condition: buffer length not 0. ENSURE( getLength() != 0 );

}

MyClass( const MyClass& aCopy ) : DBCObject( 123456 )

{

//Precondition: aCopy is valid. IS_VALID(&aCopy);

//Precondition: aCopy._buffer not NULL. REQUIRE( aCopy.getBuffer() != NULL );

//Precondition: aCopy._bufLen not 0. REQUIRE( aCopy.getLength() != 0 );

//Set the pieces.

setBuffer( aCopy._buffer ); setLength( aCopy._bufLen );

//Post-condition: buffer not NULL. ENSURE( getBuffer() != NULL );

//Post-condition: buffer length not 0. ENSURE( getLength() != 0 );

(continued)

396 Technique 63: Creating Debugging Macros and Classes

LISTING 63-4 (continued)

}

MyClass operator=( const MyClass& aCopy )

{

//Precondition: aCopy is valid. IS_VALID(&aCopy);

//Precondition: aCopy._buffer not NULL. REQUIRE( aCopy.getBuffer() != NULL );

//Precondition: aCopy._bufLen not 0. REQUIRE( aCopy.getLength() != 0 );

//Set the pieces.

setBuffer( aCopy._buffer ); setLength( aCopy._bufLen );

//Post-condition: buffer not NULL. ENSURE( getBuffer() != NULL );

//Post-condition: buffer length not 0. ENSURE( getLength() != 0 );

//Return the current object.

return *this;

}

virtual ~MyClass()

{

//Precondition: Magic number must be correct. REQUIRE( Magic() == 123456 );

//Pre-condition: length >= 0.

REQUIRE( getLength() >= 0 );

//Pre-condition: If length, buffer NOT NULL. if ( getLength() )

REQUIRE ( getBuffer() != NULL );

//All looks okay; delete the buffer.

if (

buffer != NULL )

delete [] _buffer;

_buffer

= NULL;

 

//Clear the length. _bufLen = 0;

//Post-condition: The magic number is still correct. ENSURE( Magic() == 123456 );

//Post-condition: Buffer NULL.

ENSURE( getBuffer() == NULL ); // Post-condition: Length 0. ENSURE( getLength() == 0 );

5

6

}

Design by Contract 397

void setBuffer( const char *strIn )

{

// Precondition: strIn not NULL. REQUIRE( strIn != NULL );

if ( strIn != NULL )

{

_buffer = new char[ strlen(strIn) + 1 ]; strcpy ( _buffer, strIn );

}

}

void setLength ( int length )

{

// Pre-condition: Length > 0. REQUIRE ( length > 0 );

_bufLen = length;

}

int getLength( void ) const

{

// No conditions. return _bufLen;

}

const char *getBuffer( void ) const

{

// No conditions. return _buffer;

}

};

In Listing 63-4, we use the preand post-conditions to insure that valid values are set in our object at all times. Note, for example, in the destructor for the class that our precondition is that the magic number stored in the class be set to the correct values (123456, shown at 5) and that at the end of the class the length be zero (shown at 6).

If either of these conditions is false, the program prints an error message and exits.

As you can see, the code does copious checking to make sure that the object data is always in a consistent state, that the input to the various methods is valid, and that the output from the object processes is valid as well. In the following steps, a very simple test of the code shows exactly how this all works.

3. In the code editor of your choice, reopen the source file to hold the code for your test program.

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

4. Type the following code into your file.

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

int main(int argc, char **argv )

{

// Program Conditions. REQUIRE ( argc > 1 ); REQUIRE ( argv[1] != NULL );

398 Technique 63: Creating Debugging Macros and Classes

//Empty object. MyClass mc1;

//Object defined from command line.

MyClass mc2( argv[1] );

//Make a copy of it.

MyClass mc3 = mc2;

}

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

6. Compile the source file with your favorite compiler on your favorite operating system.

7. Run the application.

If you have done everything properly, you should see the following output on your command console:

$ ./a.exe

File: ch8_1c.cpp Line: 204 Expression Failed: argc > 1

Okay, it’s fairly obvious what happened here: We told the system to check for the number of arguments to the application, and required that there be some arguments, but they weren’t there. Let’s supply one and see what happens.

$ ./a.exe Hello

File: ch8_1c.cpp Line: 99 Expression Failed: getLength() != 0

Oops! What happened here? We supplied an argument — and the code failed anyway. Looking at the line that failed, we can see that it was the post-condition of the constructor for the class. Aha! We never set the length of the buffer in the constructor, setting only the buffer itself. Let’s fix that by adding some to the constructor.

8. In the code editor of your choice, reopen the source file to hold the code for your test program.

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

9. Modify the code as follows. Replace the existing code in the MyClass constructor with the listing below.

MyClass( const char *strIn ) : DBCObject( 123456 )

{

// Precondition: strIn not

NULL.

REQUIRE( strIn != NULL );

Init();

setBuffer( strIn ); setLength ( strlen(strIn) );

// Post-condition: buffer not

NULL.

ENSURE( getBuffer() != NULL ); // Post-condition: buffer

length not 0.

ENSURE( getLength() != 0 );

}

10. Save the source file in your code editor, and then close the editor application.

11. Compile the source file with your favorite compiler on your favorite operating system.

12. Run the application.

If you have done everything properly, you should see the following output on your command console:

$ ./a.exe

As you can see, there are no errors reported. That significant lack means all the contracts in the code have been satisfied — and the code should encounter no problems when run with the tests we’ve created.

Of course, to keep this happy arrangement going, you have to exercise some care: Every time you encounter a problem in the system, make sure that you add a test to account for that problem. That is the final piece of the Design by Contract method of debugging and maintenance.

64 Debugging

Overloaded

Technique Methods

Save Time By

Debugging overloaded methods

Adding logging to an application

Handling errors

When you are debugging a program, there is nothing quite as frustrating as discovering that all the work you spent tracking down a particular problem was wasted because the method you thought was being called in a C++ class was, in fact, not the code being executed at

all. Figuring out which method is being called can be an annoying problem, and careful observation is often needed to see what is really happening, as we will see in this programming technique.

An overloaded method is a method that has the same name as another method in the same class, but contains a different number or type of arguments. (The list of arguments and the return type combine to form a signature for the method.)

When you have a class that contains overloaded methods, it is essential that you know which one is being called in each case. Because the number of arguments can be the same for different overloaded methods (only the type of the arguments is different), it can be difficult to tell which method is being called in your application source code. Let’s take a look at a class that contains overloaded methods with problems. We will create a class that contains several overloaded methods in this technique, and differentiate them only by the type of argument they accept. You will see that it is not always easy to tell which method is being called in your program. Here’s how:

1. In the code editor of your choice, create a new file to hold the code for the implementation of the source file.

In this example, the file is named ch64.cpp, although you can use whatever you choose.

2. Type the code from Listing 64-1 into your file.

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

400 Technique 64: Debugging Overloaded Methods

LISTING 64-1: CLASS WITH OVERLOADED METHODS

#include <iostream>

 

 

using namespace std;

 

 

class MyClass

 

 

{

 

 

int _x;

 

 

public:

 

 

MyClass(void)

 

 

{

 

 

_x = 0;

 

 

}

 

 

MyClass(int x)

 

 

{

 

 

_x = x;

 

 

}

 

 

MyClass( const MyClass& aCopy )

 

 

{

 

 

_x = aCopy._x;

 

 

}

 

4

MyClass operator=(int x)

 

{

 

setX(x);

 

 

return *this;

 

 

}

 

5

MyClass operator=(double d)

 

{

 

setX(d);

 

 

return *this;

 

 

}

 

6

MyClass operator=( const char *str )

 

{

 

setX( str );

 

 

return *this;

 

 

}

 

1

void setX( int x )

 

{

 

_x = x;

 

 

}

 

2

void setX( double d )

 

{

 

_x = (int)d;

 

 

}

 

3

void setX( const char *str )

 

{

 

int x = atoi(str);

}

int getX(void)

{

return _x;

}

};

void print(MyClass& mc)

{

cout

<<

“Dump: “ << endl;

cout

<<

“-----------------------” <<

endl;

cout

<<

“X = “ << mc.getX() << endl;

cout

<<

“-----------------------” <<

endl;

}

 

 

 

 

int main(int argc, char **argv)

 

 

{

 

 

 

 

MyClass mc(3);

 

 

 

 

print(mc);

 

 

 

 

mc = 2.0;

 

 

 

 

print(mc);

 

 

 

 

mc = 5;

 

 

 

 

print(mc);

 

 

 

7

mc = “6.34”;

 

 

 

print(mc);

 

 

 

}

 

 

 

 

 

The class shown in our little test program above

has three methods that have the same name,

 

setX. These methods take three different types

 

of arguments: The first method, shown at

1,

 

takes an integer value; the second method, shown

at 2, takes a floating point (double) value; the

final version of the method takes a string as its

 

argument, as shown at

3. In addition, the func-

tion has three overloaded assignment operators

 

(operator=, shown at lines 4,

5, and

6).

The test driver assigns an object

of the MyClass

 

type some various values, 2.0, 5, and “6.34”.

 

You would expect that the result of the output

 

in the print statements would be the values

 

assigned at each stage. As we will see, however,

 

the last assignment statement (shown at

7)

 

does not work properly. Take a moment and look at the code and see if you can figure out why.

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

4. Compile the source code with your favorite compiler on your favorite operating system.

5. Run the resulting program on your favorite operating system.