- •Introduction
- •Saving Time with This Book
- •Conventions Used in This Book
- •Part II: Working with the Pre-Processor
- •Part III: Types
- •Part IV: Classes
- •Part V: Arrays and Templates
- •Part VI: Input and Output
- •Part VII: Using the Built-in Functionality
- •Part VIII: Utilities
- •Part IX: Debugging C++ Applications
- •Part X: The Scary (or Fun!) Stuff
- •Icons Used in This Book
- •Creating and Implementing an Encapsulated Class
- •Creating a Mailing-List Application
- •Testing the Mailing-List Application
- •Customizing a Class with Polymorphism
- •Testing the Virtual Function Code
- •Why Do the Destructors Work?
- •Delayed Construction
- •The cDate Class
- •Testing the cDate Class
- •Creating the Header File
- •Testing the Header File
- •The Assert Problem
- •Fixing the Assert Problem
- •Using the const Construct
- •Identifying the Errors
- •Fixing the Errors
- •Fixing What Went Wrong with the Macro
- •Using Macros Appropriately
- •Using the sizeof Function
- •Evaluating the Results
- •Using sizeof with Pointers
- •Implementing the Range Class
- •Testing the Range Class
- •Creating the Matrix Class
- •Matrix Operations
- •Multiplying a Matrix by a Scalar Value
- •Multiplying a Matrix by Scalar Values, Take 2
- •Testing the Matrix Class
- •Implementing the Enumeration Class
- •Testing the Enumeration Class
- •Implementing Structures
- •Interpreting the Output
- •Defining Constants
- •Testing the Constant Application
- •Using the const Keyword
- •Illustrating Scope
- •Interpreting the Output
- •Using Casts
- •Addressing the Compiler Problems
- •Testing the Changes
- •Implementing Member-Function Pointers
- •Updating Your Code with Member-Function Pointers
- •Testing the Member Pointer Code
- •Customizing Functions We Wrote Ourselves
- •Testing the Default Code
- •Fixing the Problem
- •Testing the Complete Class
- •Implementing Virtual Inheritance
- •Correcting the Code
- •Rules for Creating Overloaded Operators
- •Using Conversion Operators
- •Using Overloaded Operators
- •Testing the MyString Class
- •Rules for Implementing new and delete Handlers
- •Overloading new and delete Handlers
- •Testing the Memory Allocation Tracker
- •Implementing Properties
- •Testing the Property Class
- •Implementing Data Validation with Classes
- •Testing Your SSN Validator Class
- •Creating the Date Class
- •Testing the Date Class
- •Some Final Thoughts on the Date Class
- •Creating a Factory Class
- •Testing the Factory
- •Enhancing the Manager Class
- •Implementing Mix-In Classes
- •Testing the Template Classes
- •Implementing Function Templates
- •Creating Method Templates
- •Using the Vector Class
- •Creating the String Array Class
- •Working with Vector Algorithms
- •Creating an Array of Heterogeneous Objects
- •Creating the Column Class
- •Creating the Row Class
- •Creating the Spreadsheet Class
- •Testing Your Spreadsheet
- •Working with Streams
- •Testing the File-Reading Code
- •Creating the Test File
- •Reading Delimited Files
- •Testing the Code
- •Creating the XML Writer
- •Testing the XML Writer
- •Creating the Configuration-File Class
- •Setting Up Your Test File
- •Building the Language Files
- •Creating an Input Text File
- •Reading the International File
- •Testing the String Reader
- •Creating a Translator Class
- •Testing the Translator Class
- •Creating a Virtual File Class
- •Testing the Virtual File Class
- •Using the auto_ptr Class
- •Creating a Memory Safe Buffer Class
- •Throwing and Logging Exceptions
- •Dealing with Unhandled Exceptions
- •Re-throwing Exceptions
- •Creating the Wildcard Matching Class
- •Testing the Wildcard Matching Class
- •Creating the URL Codec Class
- •Testing the URL Codec Class
- •Testing the Rot13 Algorithm
- •Testing the XOR Algorithm
- •Implementing the transform Function to Convert Strings
- •Testing the String Conversions
- •Implementing the Serialization Interface
- •Creating the Buffer Class
- •Testing the Buffer Class
- •Creating the Multiple-Search-Path Class
- •Testing the Multiple-Search-Path Class
- •Testing the Flow Trace System
- •The assert Macro
- •Logging
- •Testing the Logger Class
- •Design by Contract
- •Adding Logging to the Application
- •Making Functions Inline
- •Avoiding Temporary Objects
- •Passing Objects by Reference
- •Choosing Initialization Instead of Assignment
- •Learning How Code Operates
- •Testing the Properties Class
- •Creating the Locking Mechanism
- •Testing the Locking Mechanism
- •Testing the File-Guardian Class
- •Implementing the Complex Class
- •Creating the Conversion Code
- •Testing the Conversion Code
- •A Sample Program
- •Componentizing
- •Restructuring
- •Specialization
- •Index
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.