
- •Contents
- •Introduction
- •Who This Book Is For
- •What This Book Covers
- •How This Book Is Structured
- •What You Need to Use This Book
- •Conventions
- •Source Code
- •Errata
- •p2p.wrox.com
- •The Basics of C++
- •The Obligatory Hello, World
- •Namespaces
- •Variables
- •Operators
- •Types
- •Conditionals
- •Loops
- •Arrays
- •Functions
- •Those Are the Basics
- •Diving Deeper into C++
- •Pointers and Dynamic Memory
- •Strings in C++
- •References
- •Exceptions
- •The Many Uses of const
- •C++ as an Object-Oriented Language
- •Declaring a Class
- •Your First Useful C++ Program
- •An Employee Records System
- •The Employee Class
- •The Database Class
- •The User Interface
- •Evaluating the Program
- •What Is Programming Design?
- •The Importance of Programming Design
- •Two Rules for C++ Design
- •Abstraction
- •Reuse
- •Designing a Chess Program
- •Requirements
- •Design Steps
- •An Object-Oriented View of the World
- •Am I Thinking Procedurally?
- •The Object-Oriented Philosophy
- •Living in a World of Objects
- •Object Relationships
- •Abstraction
- •Reusing Code
- •A Note on Terminology
- •Deciding Whether or Not to Reuse Code
- •Strategies for Reusing Code
- •Bundling Third-Party Applications
- •Open-Source Libraries
- •The C++ Standard Library
- •Designing with Patterns and Techniques
- •Design Techniques
- •Design Patterns
- •The Reuse Philosophy
- •How to Design Reusable Code
- •Use Abstraction
- •Structure Your Code for Optimal Reuse
- •Design Usable Interfaces
- •Reconciling Generality and Ease of Use
- •The Need for Process
- •Software Life-Cycle Models
- •The Stagewise and Waterfall Models
- •The Spiral Method
- •The Rational Unified Process
- •Software-Engineering Methodologies
- •Extreme Programming (XP)
- •Software Triage
- •Be Open to New Ideas
- •Bring New Ideas to the Table
- •Thinking Ahead
- •Keeping It Clear
- •Elements of Good Style
- •Documenting Your Code
- •Reasons to Write Comments
- •Commenting Styles
- •Comments in This Book
- •Decomposition
- •Decomposition through Refactoring
- •Decomposition by Design
- •Decomposition in This Book
- •Naming
- •Choosing a Good Name
- •Naming Conventions
- •Using Language Features with Style
- •Use Constants
- •Take Advantage of const Variables
- •Use References Instead of Pointers
- •Use Custom Exceptions
- •Formatting
- •The Curly Brace Alignment Debate
- •Coming to Blows over Spaces and Parentheses
- •Spaces and Tabs
- •Stylistic Challenges
- •Introducing the Spreadsheet Example
- •Writing Classes
- •Class Definitions
- •Defining Methods
- •Using Objects
- •Object Life Cycles
- •Object Creation
- •Object Destruction
- •Assigning to Objects
- •Distinguishing Copying from Assignment
- •The Spreadsheet Class
- •Freeing Memory with Destructors
- •Handling Copying and Assignment
- •Different Kinds of Data Members
- •Static Data Members
- •Const Data Members
- •Reference Data Members
- •Const Reference Data Members
- •More about Methods
- •Static Methods
- •Const Methods
- •Method Overloading
- •Default Parameters
- •Inline Methods
- •Nested Classes
- •Friends
- •Operator Overloading
- •Implementing Addition
- •Overloading Arithmetic Operators
- •Overloading Comparison Operators
- •Building Types with Operator Overloading
- •Pointers to Methods and Members
- •Building Abstract Classes
- •Using Interface and Implementation Classes
- •Building Classes with Inheritance
- •Extending Classes
- •Overriding Methods
- •Inheritance for Reuse
- •The WeatherPrediction Class
- •Adding Functionality in a Subclass
- •Replacing Functionality in a Subclass
- •Respect Your Parents
- •Parent Constructors
- •Parent Destructors
- •Referring to Parent Data
- •Casting Up and Down
- •Inheritance for Polymorphism
- •Return of the Spreadsheet
- •Designing the Polymorphic Spreadsheet Cell
- •The Spreadsheet Cell Base Class
- •The Individual Subclasses
- •Leveraging Polymorphism
- •Future Considerations
- •Multiple Inheritance
- •Inheriting from Multiple Classes
- •Naming Collisions and Ambiguous Base Classes
- •Interesting and Obscure Inheritance Issues
- •Special Cases in Overriding Methods
- •Copy Constructors and the Equals Operator
- •The Truth about Virtual
- •Runtime Type Facilities
- •Non-Public Inheritance
- •Virtual Base Classes
- •Class Templates
- •Writing a Class Template
- •How the Compiler Processes Templates
- •Distributing Template Code between Files
- •Template Parameters
- •Method Templates
- •Template Class Specialization
- •Subclassing Template Classes
- •Inheritance versus Specialization
- •Function Templates
- •Function Template Specialization
- •Function Template Overloading
- •Friend Function Templates of Class Templates
- •Advanced Templates
- •More about Template Parameters
- •Template Class Partial Specialization
- •Emulating Function Partial Specialization with Overloading
- •Template Recursion
- •References
- •Reference Variables
- •Reference Data Members
- •Reference Parameters
- •Reference Return Values
- •Deciding between References and Pointers
- •Keyword Confusion
- •The const Keyword
- •The static Keyword
- •Order of Initialization of Nonlocal Variables
- •Types and Casts
- •typedefs
- •Casts
- •Scope Resolution
- •Header Files
- •C Utilities
- •Variable-Length Argument Lists
- •Preprocessor Macros
- •How to Picture Memory
- •Allocation and Deallocation
- •Arrays
- •Working with Pointers
- •Array-Pointer Duality
- •Arrays Are Pointers!
- •Not All Pointers Are Arrays!
- •Dynamic Strings
- •C-Style Strings
- •String Literals
- •The C++ string Class
- •Pointer Arithmetic
- •Custom Memory Management
- •Garbage Collection
- •Object Pools
- •Function Pointers
- •Underallocating Strings
- •Memory Leaks
- •Double-Deleting and Invalid Pointers
- •Accessing Out-of-Bounds Memory
- •Using Streams
- •What Is a Stream, Anyway?
- •Stream Sources and Destinations
- •Output with Streams
- •Input with Streams
- •Input and Output with Objects
- •String Streams
- •File Streams
- •Jumping around with seek() and tell()
- •Linking Streams Together
- •Bidirectional I/O
- •Internationalization
- •Wide Characters
- •Non-Western Character Sets
- •Locales and Facets
- •Errors and Exceptions
- •What Are Exceptions, Anyway?
- •Why Exceptions in C++ Are a Good Thing
- •Why Exceptions in C++ Are a Bad Thing
- •Our Recommendation
- •Exception Mechanics
- •Throwing and Catching Exceptions
- •Exception Types
- •Throwing and Catching Multiple Exceptions
- •Uncaught Exceptions
- •Throw Lists
- •Exceptions and Polymorphism
- •The Standard Exception Hierarchy
- •Catching Exceptions in a Class Hierarchy
- •Writing Your Own Exception Classes
- •Stack Unwinding and Cleanup
- •Catch, Cleanup, and Rethrow
- •Use Smart Pointers
- •Common Error-Handling Issues
- •Memory Allocation Errors
- •Errors in Constructors
- •Errors in Destructors
- •Putting It All Together
- •Why Overload Operators?
- •Limitations to Operator Overloading
- •Choices in Operator Overloading
- •Summary of Overloadable Operators
- •Overloading the Arithmetic Operators
- •Overloading Unary Minus and Unary Plus
- •Overloading Increment and Decrement
- •Overloading the Subscripting Operator
- •Providing Read-Only Access with operator[]
- •Non-Integral Array Indices
- •Overloading the Function Call Operator
- •Overloading the Dereferencing Operators
- •Implementing operator*
- •Implementing operator->
- •What in the World Is operator->* ?
- •Writing Conversion Operators
- •Ambiguity Problems with Conversion Operators
- •Conversions for Boolean Expressions
- •How new and delete Really Work
- •Overloading operator new and operator delete
- •Overloading operator new and operator delete with Extra Parameters
- •Two Approaches to Efficiency
- •Two Kinds of Programs
- •Is C++ an Inefficient Language?
- •Language-Level Efficiency
- •Handle Objects Efficiently
- •Use Inline Methods and Functions
- •Design-Level Efficiency
- •Cache as Much as Possible
- •Use Object Pools
- •Use Thread Pools
- •Profiling
- •Profiling Example with gprof
- •Cross-Platform Development
- •Architecture Issues
- •Implementation Issues
- •Platform-Specific Features
- •Cross-Language Development
- •Mixing C and C++
- •Shifting Paradigms
- •Linking with C Code
- •Mixing Java and C++ with JNI
- •Mixing C++ with Perl and Shell Scripts
- •Mixing C++ with Assembly Code
- •Quality Control
- •Whose Responsibility Is Testing?
- •The Life Cycle of a Bug
- •Bug-Tracking Tools
- •Unit Testing
- •Approaches to Unit Testing
- •The Unit Testing Process
- •Unit Testing in Action
- •Higher-Level Testing
- •Integration Tests
- •System Tests
- •Regression Tests
- •Tips for Successful Testing
- •The Fundamental Law of Debugging
- •Bug Taxonomies
- •Avoiding Bugs
- •Planning for Bugs
- •Error Logging
- •Debug Traces
- •Asserts
- •Debugging Techniques
- •Reproducing Bugs
- •Debugging Reproducible Bugs
- •Debugging Nonreproducible Bugs
- •Debugging Memory Problems
- •Debugging Multithreaded Programs
- •Debugging Example: Article Citations
- •Lessons from the ArticleCitations Example
- •Requirements on Elements
- •Exceptions and Error Checking
- •Iterators
- •Sequential Containers
- •Vector
- •The vector<bool> Specialization
- •deque
- •list
- •Container Adapters
- •queue
- •priority_queue
- •stack
- •Associative Containers
- •The pair Utility Class
- •multimap
- •multiset
- •Other Containers
- •Arrays as STL Containers
- •Strings as STL Containers
- •Streams as STL Containers
- •bitset
- •The find() and find_if() Algorithms
- •The accumulate() Algorithms
- •Function Objects
- •Arithmetic Function Objects
- •Comparison Function Objects
- •Logical Function Objects
- •Function Object Adapters
- •Writing Your Own Function Objects
- •Algorithm Details
- •Utility Algorithms
- •Nonmodifying Algorithms
- •Modifying Algorithms
- •Sorting Algorithms
- •Set Algorithms
- •The Voter Registration Audit Problem Statement
- •The auditVoterRolls() Function
- •The getDuplicates() Function
- •The RemoveNames Functor
- •The NameInList Functor
- •Testing the auditVoterRolls() Function
- •Allocators
- •Iterator Adapters
- •Reverse Iterators
- •Stream Iterators
- •Insert Iterators
- •Extending the STL
- •Why Extend the STL?
- •Writing an STL Algorithm
- •Writing an STL Container
- •The Appeal of Distributed Computing
- •Distribution for Scalability
- •Distribution for Reliability
- •Distribution for Centrality
- •Distributed Content
- •Distributed versus Networked
- •Distributed Objects
- •Serialization and Marshalling
- •Remote Procedure Calls
- •CORBA
- •Interface Definition Language
- •Implementing the Class
- •Using the Objects
- •A Crash Course in XML
- •XML as a Distributed Object Technology
- •Generating and Parsing XML in C++
- •XML Validation
- •Building a Distributed Object with XML
- •SOAP (Simple Object Access Protocol)
- •. . . Write a Class
- •. . . Subclass an Existing Class
- •. . . Throw and Catch Exceptions
- •. . . Read from a File
- •. . . Write to a File
- •. . . Write a Template Class
- •There Must Be a Better Way
- •Smart Pointers with Reference Counting
- •Double Dispatch
- •Mix-In Classes
- •Object-Oriented Frameworks
- •Working with Frameworks
- •The Model-View-Controller Paradigm
- •The Singleton Pattern
- •Example: A Logging Mechanism
- •Implementation of a Singleton
- •Using a Singleton
- •Example: A Car Factory Simulation
- •Implementation of a Factory
- •Using a Factory
- •Other Uses of Factories
- •The Proxy Pattern
- •Example: Hiding Network Connectivity Issues
- •Implementation of a Proxy
- •Using a Proxy
- •The Adapter Pattern
- •Example: Adapting an XML Library
- •Implementation of an Adapter
- •Using an Adapter
- •The Decorator Pattern
- •Example: Defining Styles in Web Pages
- •Implementation of a Decorator
- •Using a Decorator
- •The Chain of Responsibility Pattern
- •Example: Event Handling
- •Implementation of a Chain of Responsibility
- •Using a Chain of Responsibility
- •Example: Event Handling
- •Implementation of an Observer
- •Using an Observer
- •Chapter 1: A Crash Course in C++
- •Chapter 3: Designing with Objects
- •Chapter 4: Designing with Libraries and Patterns
- •Chapter 5: Designing for Reuse
- •Chapter 7: Coding with Style
- •Chapters 8 and 9: Classes and Objects
- •Chapter 11: Writing Generic Code with Templates
- •Chapter 14: Demystifying C++ I/O
- •Chapter 15: Handling Errors
- •Chapter 16: Overloading C++ Operators
- •Chapter 17: Writing Efficient C++
- •Chapter 19: Becoming Adept at Testing
- •Chapter 20: Conquering Debugging
- •Chapter 24: Exploring Distributed Objects
- •Chapter 26: Applying Design Patterns
- •Beginning C++
- •General C++
- •I/O Streams
- •The C++ Standard Library
- •C++ Templates
- •Integrating C++ and Other Languages
- •Algorithms and Data Structures
- •Open-Source Software
- •Software-Engineering Methodology
- •Programming Style
- •Computer Architecture
- •Efficiency
- •Testing
- •Debugging
- •Distributed Objects
- •CORBA
- •XML and SOAP
- •Design Patterns
- •Index

Chapter 20
Bug Taxonomies
A bug in a computer program is incorrect run-time behavior. This undesirable behavior includes both catastrophic bugs that cause program death, data corruption, operating system panics, or some other similarly horrific outcome and noncatastrophic bugs that cause the program to behave incorrectly in more subtle ways. For example, a Web browser might return the wrong Web page, or a spreadsheet application might calculate the standard deviation of a column incorrectly. The underlying cause, or root cause, of the bug is the mistake in the program that causes this incorrect behavior. The process of debugging a program includes both determining the root cause of the bug and fixing the code so that the bug will not occur again.
Programmers often use the term root-cause as a verb, as in “Have you root-caused that core dump yet?”
Avoiding Bugs
The powerful features of C++ make it an especially error-prone language, so debugging skills are even more important when coding in C++ than when using most other languages. Here are a few tips for avoiding bugs in your programs:
Read this book from cover to cover. Learn the C++ language intimately, especially pointers and memory management. Then, recommend this book to your friends and coworkers so they avoid bugs too!
Follow the style guidelines in this book, specifically those described in Chapter 7. They will lead to fewer bugs because you, and other people, will be able to understand your programs.
Design before you code. Designing while you code tends to lead to convoluted designs that are harder to understand and are more error-prone. It also makes you more likely to omit possible edge cases and error conditions.
Utilize code reviews: At least two people should look at every line of code that you write. Sometimes it takes a fresh perspective to notice problems.
Test, test, and test again. Follow the guidelines in Chapter 19.
Expect error conditions, and handle them appropriately. In particular, plan for and handle out- of-memory conditions. They will occur. See Chapter 15.
Last, and probably most importantly, use smart pointers to avoid memory leaks. See Chapters 13, 15, and 25 for details.
Planning for Bugs
Your programs should contain features that enable easier debugging when the inevitable bugs arise. This section describes these features and presents sample implementations that you can incorporate into your own programs.
Error Logging
Imagine this scenario: You have just released a new version of your flagship product, and one of the first users reports that the program “stopped working.” You attempt to pry more information from the user,
528

Conquering Debugging
and eventually discover that the program died in the middle of an operation. The user can’t quite remember what he was doing, or if there were any error messages. How will you debug this problem?
Now imagine the same scenario, but in addition to the limited information from the user, you are also able to examine the error log on the user’s computer. In the log you see a message from your program that says “Error: unable to allocate memory.” Looking at the code near the spot where that error message was generated, you find a line in which you dereferenced a pointer without checking for NULL.
You’ve found the root cause of your bug!
Error logging is the process of writing error messages to persistent storage so that they will be available following an application, or even machine, death. Despite the example scenario, you might still have doubts about this strategy. Won’t it be obvious by your program’s behavior if it encounters errors? Won’t the user notice if something goes wrong? As the preceding example shows, user reports are not always accurate or complete. In addition, many programs, such as the operating system kernel and long-running daemons like inetd or syslogd on Unix, are not interactive and run unattended on a machine. The only way these programs can communicate with users is through error logging.
Thus, your program should log errors as it encounters them. That way, if a user reports a bug, you will be able to examine the log files on the machine to see if your program reported any errors prior to encountering the bug. Unfortunately, error logging is platform dependent: C++ does not contain a standard logging mechanism. Examples of platform-specific logging mechanisms include the syslog facility in Unix and the event reporting API in Windows. You should consult the documentation for your development platform. There are also some open-source implementations of cross-platform logging classes, including log4cpp (available at http://sourceforge.net).
Now that you’re convinced that error logging is a great feature to add to your programs, you might be tempted to log error messages every few lines in your code, so that, in the event of any bug, you’ll be able to trace the code path that was executing. These types of error messages are appropriately called “traces.” However, you should not write these traces to error logs for two reasons. First, writing to persistent storage is slow. Even on systems that write the logs asynchronously, logging that much information will slow down your program. Second, and most importantly, most of the information that you would put in your traces is not appropriate for the end user to see. It will just confuse the user, leading to unwarranted service calls. That said, tracing is an important debugging technique under the correct circumstances, as described in the next section.
Here are some specific guidelines for the types of errors your programs should log:
Unrecoverable errors, such as an inability to allocate memory or a system call failing unexpectedly. These errors will usually directly precede an application exit or memory core dump.
Errors for which an administrator can take action, such as low memory, an incorrectly formatted data file, an inability to write to disk, or a network connection being down.
Unexpected errors such as a code path that you never expected to take or variables with unexpected values. Note that your code should “expect” users to enter invalid data and should handle it appropriately. An unexpected error would represent a bug in your program.
Security breaches such as a network connection attempted from an unauthorized address or too many network connections attempted (denial of service).
Additionally, most APIs allow you to specify a log level or error level. You can log nonerror conditions under a log level that is less severe than “error.” For example, you might want to log significant state
529

Chapter 20
changes in your application, or startup and shutdown of the program. You also might consider giving your users a way to adjust the log level of your program at run time so that they can customize the amount of logging that occurs.
Debug Traces
When debugging complicated problems, public error messages generally do not contain enough information. You often need a complete trace of the code path taken or values of variables before the bug showed up. In addition to basic messages, it’s sometimes helpful to include the following information in debug traces:
The thread ID, if a multithreaded program.
The name of the function that generated the trace.
The name of the source file in which the code that generates this trace lives.
You can add this tracing to your program through a special debug mode, or via a ring buffer. Both of these methods are explained in detail below.
Debug Mode
The first technique to add debug traces is to provide a debug mode for your program. In debug mode, the program writes trace output to standard error or to a file, and perhaps does extra checking during execution. There are several ways to add a debug mode to your program.
Compile-Time Debug Mode
You can use preprocessor #ifdefs to selectively compile the debug code into your program. The advantage of this method is that your debug code is not compiled into the “release” binary, and so does not increase its size. The disadvantages are that there is no way to enable debugging at a customer site for testing or following the discovery of a bug, and your code starts to look cluttered and indecipherable.
The rest of this section shows an example of a simple program instrumented with a compile-time debug mode. This program doesn’t do anything useful: it is only for demonstrating the technique.
In order to generate a debug version of the program, it should be compiled with the symbol DEBUG_MODE defined. Your compiler should allow you to specify symbols to define during compilation; consult your documentation for details. For example, g++ allows you to specify –Dsymbol on the compile command.
Note that this example uses a global variable for the ofstream object. This example is one of the only times that we recommend using global variables! It’s acceptable here because debug mode should not interfere with the rest of the program. If the ofstream object were not global, you would have to pass it to each function, requiring changes to all the function prototypes in the whole program.
// CTDebug.cpp #include <exception> #include <fstream> #include <iostream> using namespace std;
530

Conquering Debugging
#ifdef DEBUG_MODE ofstream debugOstr;
const char* debugFileName = “debugfile.out”; #endif
class ComplicatedClass
{
public: ComplicatedClass() {}
// Class details omitted for brevity
};
class UserCommand
{
public: UserCommand() {}
// Class details not shown for brevity
};
ostream& operator<<(ostream& ostr, const ComplicatedClass& src); ostream& operator<<(ostream& ostr, const UserCommand& src); UserCommand getNextCommand(ComplicatedClass* obj);
void processUserCommand(UserCommand& cmd);
void trickyFunction(ComplicatedClass* obj) throw(exception);
int main(int argc, char** argv)
{
#ifdef DEBUG_MODE
//Open the output stream. debugOstr.open(debugFileName); if (debugOstr.fail()) {
cout << “Unable to open debug file!\n”; return (1);
}
//Print the command-line arguments to the trace. for (int i = 0; i < argc; i++) {
debugOstr << argv[i] << “ “; debugOstr << endl;
}
#endif
// Rest of the function not shown return (0);
}
ostream& operator<<(ostream& ostr, const ComplicatedClass& src)
{
ostr << “ComplicatedClass”; return (ostr);
}
531

Chapter 20
ostream& operator<<(ostream& ostr, const UserCommand& src)
{
ostr << “UserCommand”; return (ostr);
}
UserCommand getNextCommand(ComplicatedClass* obj)
{
UserCommand cmd; return (cmd);
}
void processUserCommand(UserCommand& cmd)
{
// Details omitted for brevity
}
void trickyFunction(ComplicatedClass* obj) throw(exception)
{
#ifdef DEBUG_MODE
// If in debug mode, print the values with which this function starts debugOstr << “trickyFunction(): given argument: “ << *obj << endl;
#endif
while (true) {
UserCommand cmd = getNextCommand(obj);
#ifdef DEBUG_MODE
debugOstr << “trickyFunction(): retrieved cmd “ << cmd << endl;
#endif
try { processUserCommand(cmd);
} catch (exception& e) { #ifdef DEBUG_MODE
debugOstr << “trickyFunction(): “
<<“ received exception from procesUserCommand(): “
<<e.what() << endl;
#endif
throw;
}
}
}
Start-Time Debug Mode
Start-time debug mode is an alternative to #ifdefs that is just as simple to implement. A command-line argument to the program can specify whether it should run in debug mode. Unlike compile-time debug mode, this strategy includes the debug code in the “release” binary, and allows debug mode to be enabled at a customer site. However, it still requires users to restart the program in order to run it in debug mode, which is not always an attractive alternative for customers, and which may prevent you from obtaining useful information about bugs.
532

Conquering Debugging
The following example of start-time debug mode uses the same program as that shown for compile-time debug mode so that you can directly compare the differences. This version of the program again uses global variables: this time for the ofstream and the Boolean specifying whether the program is in debug mode. This choice is acceptable here to avoid imposing extra debug arguments on all the function prototypes.
One aspect of this program needs further comment: there is no standard library functionality in C++ for parsing command-line arguments. This program uses a simple function isDebugSet() to check for the debug flag among all the command-line arguments, but a function to parse all command-line arguments would need to be more sophisticated.
// STDebug.cpp #include <exception> #include <fstream> #include <iostream> using namespace std;
ofstream debugOstr; bool debug = false;
const char* debugFileName = “debugfile.out”;
class ComplicatedClass
{
public: ComplicatedClass() {} ~ComplicatedClass() {}
};
class UserCommand
{
public: UserCommand() {}
};
bool isDebugSet(int argc, char** argv);
ostream& operator<<(ostream& ostr, const ComplicatedClass& src); ostream& operator<<(ostream& ostr, const UserCommand& src); UserCommand getNextCommand(ComplicatedClass* obj);
void processUserCommand(UserCommand& cmd);
void trickyFunction(ComplicatedClass* obj) throw(exception);
int main(int argc, char** argv)
{
debug = isDebugSet(argc, argv);
if (debug) {
//Open the output stream. debugOstr.open(debugFileName); if (debugOstr.fail()) {
cout << “Unable to open debug file!\n”; return (1);
}
//Print the command-line arguments.
533

Chapter 20
for (int i = 0; i < argc; i++) { debugOstr << argv[i] << “ “; debugOstr << endl;
}
}
// Rest of the function not shown return (0);
}
bool isDebugSet(int argc, char** argv)
{
for (int i = 0; i < argc; i++) {
if (strcmp(argv[i], “-d”) == 0) { return (true);
}
}
return (false);
}
ostream& operator<<(ostream& ostr, const ComplicatedClass& src)
{
ostr << “ComplicatedClass”; return (ostr);
}
ostream& operator<<(ostream& ostr, const UserCommand& src)
{
ostr << “UserCommand”; return (ostr);
}
UserCommand getNextCommand(ComplicatedClass* obj)
{
UserCommand cmd; return (cmd);
}
void processUserCommand(UserCommand& cmd)
{
// Details omitted for brevity
}
void trickyFunction(ComplicatedClass* obj) throw(exception)
{
if (debug) {
// If in debug mode, print the values with which this function starts debugOstr << “trickyFunction(): given argument: “ << *obj << endl;
}
while (true) {
UserCommand cmd = getNextCommand(obj); if (debug) {
debugOstr << “trickyFunction(): retrieved cmd “ << cmd << endl;
}
534

Conquering Debugging
try { processUserCommand(cmd);
} catch (exception& e) { if (debug) {
debugOstr << “trickyFunction(): “
<<“ received exception from procesUserCommand(): “
<<e.what() << endl;
}
throw;
}
}
}
Run-Time Debug Mode
The most flexible way to provide a debug mode is to allow it to be enabled or disabled at run time. One way to provide this feature is to supply an asynchronous interface that controls debug mode on the fly. In a GUI program, this interface could take the form of a menu command. In a CLI program, this interface could be an asynchronous command that makes an interprocess call into the program (using sockets, signals, or remote procedure calls for example). C++ provides no standard way to perform interprocess communication or GUIs, so we do not show an example of this technique.
Ring Buffers
Debug mode is useful for debugging reproducible problems and for running tests. However, bugs often appear when the program is running in nondebug mode, and by the time you or the customer enables debug mode, it is too late to gain any information about the bug. One solution to this problem is to enable tracing in your program at all times. You usually need only the most recent traces to debug a program, so you should store only the most recent traces at any point in a program’s execution. One way to provide this limitation is through careful use of log file rotations.
However, in order to avoid the problems with logging traces described earlier in the “Error Logging” section, it is better if your program doesn’t log these traces at all; it should store them in memory. Then, it should provide a mechanism to dump all the trace messages to standard error or to a log file if the need arises. A common technique is to use a ring buffer to store a fixed number of messages, or messages in a fixed amount of memory. When the buffer fills up, it starts writing messages at the beginning of the buffer again, overwriting the older messages. This cycle can repeat indefinitely. The following sections provide an implementation of a ring buffer and show you how you can use it in your programs.
Ring Buffer Interface
#include <vector> #include <string> #include <fstream>
using std::string; using std::vector; using std::ostream;
//
//class RingBuffer
//Provides a simple debug buffer. The client specifies the number
//of entries in the constructor and adds messages with the addEntry()
535

Chapter 20
//method. Once the number of entries exceeds the number allowed, new
//entries overwrite the oldest entries in the buffer.
//
//The buffer also provides the option to print entries as they
//are added to the buffer. The client can specify an output stream
//in the constructor, and can reset it with the setOutput() method.
//Finally, the buffer supports streaming to an output stream.
//
class RingBuffer
{
public:
//
//Constructs a ring buffer with space for numEntries.
//Entries are written to *ostr as they are queued.
RingBuffer(int numEntries = kDefaultNumEntries, ostream* ostr = NULL); ~RingBuffer();
//
//Adds the string to the ring buffer, possibly overwriting the
//oldest string in the buffer (if the buffer is full).
//
void addEntry(const string& entry);
//
// Streams the buffer entries, separated by newlines, to ostr.
//
friend ostream& operator<<(ostream& ostr, const RingBuffer& rb);
//
//Sets the output stream to which entries are streamed as they are added.
//Returns the old output stream.
//
ostream* setOutput(ostream* newOstr);
protected:
vector<string> mEntries; ostream* mOstr;
int mNumEntries, mNext; bool mWrapped;
static const int kDefaultNumEntries = 500;
private:
// Prevent assignment and pass-by-value. RingBuffer(const RingBuffer& src); RingBuffer& operator=(const RingBuffer& rhs);
};
Ring Buffer Implementation
This implementation of the ring buffer stores a fixed number of strings. Each of these strings must be copied into the ring buffer, requiring dynamic allocation of memory. This approach certainly is not the
536

Conquering Debugging
most efficient solution. Other possibilities would be to provide a fixed number of bytes of memory for the buffer. However, that requires mucking with low-level C-strings and memory management, which you should avoid whenever possible. This implementation should be sufficient unless you’re writing a high-performance application.
This ring buffer uses the STL vector to store the string entries. You could also use a standard C-style array. The use of the STL is straightforward except for the implementation of operator<< for the RingBuffer, which employs some fancy iterators. Consult Chapters 21, 22, and 23 for the details of iterators and the copy algorithm.
#include <algorithm> #include <iterator> #include <iostream> #include “RingBuffer.h”
using namespace std;
const int RingBuffer::kDefaultNumEntries;
//
//Initialize the vector to hold exactly numEntries. The vector size
//does not need to change during the lifetime of the object.
//
// Initialize the other members.
//
RingBuffer::RingBuffer(int numEntries, ostream* ostr) : mEntries(numEntries), mOstr(ostr), mNumEntries(numEntries), mNext(0), mWrapped(false)
{
}
RingBuffer::~RingBuffer()
{
}
//
//The algorithm is pretty simple: add the entry to the next
//free spot, then reset mNext to indicate the free spot after
//that. If mNext reaches the end of the vector, it starts over at 0.
//The buffer needs to know if the buffer has wrapped or not so
//that it knows whether to print the entries past mNext in operator<<
void RingBuffer::addEntry(const string& entry)
{
//Add the entry to the next free spot and increment
//mNext to point to the free spot after that.
mEntries[mNext++] = entry;
//Check if we’ve reached the end of the buffer. If so, we need to wrap. if (mNext >= mNumEntries) {
mNext = 0; mWrapped = true;
}
//If there is a valid ostream, write this entry to it.
537

Chapter 20
if (mOstr != NULL) {
*mOstr << entry << endl;
}
}
ostream* RingBuffer::setOutput(ostream* newOstr)
{
ostream* ret = mOstr; mOstr = newOstr; return (ret);
}
//
// This function uses an ostream_iterator to “copy” entries directly
//from the vector to the output stream.
//This function must print the entries in order. If the buffer has wrapped,
//the earliest entry is one past the most recent entry, which is the entry
//indicated by mNext. So first print from entry mNext to the end.
//
// Then (even if the buffer hasn’t wrapped) print from the beginning to mNext - 1.
//
ostream& operator<<(ostream& ostr, const RingBuffer& rb)
{
if (rb.mWrapped) {
//
//If the buffer has wrapped, print the elements from
//the earliest entry to the end.
//
copy (rb.mEntries.begin() + rb.mNext, rb.mEntries.end(), ostream_iterator<string>(ostr, “\n”));
}
//
//Now print up to the most recent entry.
//Go up to begin() + mNext because the range is not inclusive on the
//right side.
//
copy (rb.mEntries.begin(), rb.mEntries.begin() + rb.mNext, ostream_iterator<string>(ostr, “\n”));
return (ostr);
}
Using the Ring Buffer
In order to use the ring buffer, you can simply declare an object and start adding messages to it. When you want to print the buffer, just use operator<< to print it to the appropriate ostream. Here is the earlier start-time debug mode program modified to show use of a ring buffer instead:
#include “RingBuffer.h” #include <exception> #include <fstream> #include <iostream> #include <cassert>
538

Conquering Debugging
#include <sstream> using namespace std;
RingBuffer debugBuf;
class ComplicatedClass
{
public: ComplicatedClass() {} ~ComplicatedClass() {}
};
class UserCommand
{
public: UserCommand() {}
};
ostream& operator<<(ostream& ostr, const ComplicatedClass& src); ostream& operator<<(ostream& ostr, const UserCommand& src); UserCommand getNextCommand(ComplicatedClass* obj);
void processUserCommand(UserCommand& cmd);
void trickyFunction(ComplicatedClass* obj) throw(exception);
int main(int argc, char** argv)
{
// Print the command-line arguments. for (int i = 0; i < argc; i++) {
debugBuf.addEntry(argv[i]);
}
trickyFunction(new ComplicatedClass());
// Print the current contents of the debug buffer to cout.
cout << debugBuf;
return (0);
}
ostream& operator<<(ostream& ostr, const ComplicatedClass& src)
{
ostr << “ComplicatedClass”; return (ostr);
}
ostream& operator<<(ostream& ostr, const UserCommand& src)
{
ostr << “UserCommand”; return (ostr);
}
UserCommand getNextCommand(ComplicatedClass* obj)
{
UserCommand cmd; return (cmd);
}
539