You don't have to sacrifice the convenience of Iostreams while debugging, at least not if they're packaged properly.
Introduction
Most programs we would call "useful" include some way to report, record, and/or respond to error conditions. Methods of displaying errors vary widely, including output to the console, pop-up dialog boxes, or static message areas, to name a few. However, the basic need is the same time after time: to display some textual message in response to an unexpected condition. Many mission-critical applications also include an error logging facility that the developer can use to trace the source of a problem after the fact.
The need for error reporting and tracing is often greatest during the early stages of development, when the programmer is still trying out prototypes and off-the-cuff code snippets. Unfortunately, this early error handling code is often tacked on just for debugging purposes, and is either discarded later as the code matures, or is replaced by one or more iterations of customized error handling functions.
For these reasons, I decided to create a simple, standardized, general-purpose error reporting and logging tool that I could use for all stages of the software development cycle, from design through production. I have developed this tool as an extension of the standard iostream library, partly as a way to learn some of the deeper nuances of iostreams. The result of this effort is a class called Errstream and a stream object called errout, which can be used in place of the standard stream cerr.
Errstream provides a simple, standard method for displaying and logging errors and debug information. I use it for everything from quick prototypes and test drivers to large-scale production applications. In this article, I describe the design of the Errstream class and the implementation of its various features. I also attempt to pass on some of the things I learned about extending iostreams.
iostream vs. stdio
I had two main reasons for choosing iostream instead of stdio as the basis for my error handler: type-safety and extensibility [1]. stdio's printf family of functions is tried and true, and is often the most convenient way to perform a given output task, particularly when specific formatting is desired. However, the convenience of these functions comes at the expense of type safety. If you put the wrong type specifier in the format string, your output may be wrong and the error may not be obvious. The iostream functions, on the other hand, provide type safety at compile time (at the expense of code that is arguably a bit less efficient, more complex, and harder to read).
The other advantage to iostream is the extensibility provided by its object-oriented nature. Errstream benefits from this nature by inheriting the entire interface and capabilities of the standard class ofstream for writing to a file.
Class Errstream
Listings 1 and 2 show the interface and implementation of the Errstream class. The listings are abbreviated here to save space. The full source code is available from the CUJ ftp site (see p. 3 for downloading instructions). I will explain the design and each of the features in the class as they appear in the listings.
Errstream inherits publicly from the standard library class ofstream, which provides the log file writing capability. Errstream provides two constructors, one that opens a user-specified log file, and a default constructor that opens a default log file in the current working directory. The default constructor shown in Listing 2 appends the (Unix) process id number as an extension to the default log file name. This prevents overwriting the log file across multiple program runs. A non-Unix developer would of course have to remove or replace this feature. The destructor closes the log file after flushing any pending output.
The open method first closes any existing log file if one is already open, then creates a new log file with the specified name. Note that the constructors do not actually open the log file; the file is not opened until it is needed for output by one of the inserter operators. Thus, the program creates an error log file only if it generates errors.
The errString and clear methods provide direct control over the internal error string. The average user should not have much use for these methods, but other parts of the Errstream class depend on them, as I show later. The errString method returns a read-only pointer to the current error string in the form of a null-terminated character array. The clear method sets the error string to an empty string by setting its first element to null.
As the implementations of these two methods show, I use a fixed-length character array for internal storage of the error string. I would have preferred to use the string class from the standard library, but the simple numeric-to-string formatting capabilities of sprintf were just too tempting. Conversion to the string class after formatting with sprintf would allow error strings of unlimited size, but I felt that this benefit was outweighed by the added complexity.
Actually, the ideal choice for storing an error string would have been a stringstream, either via multiple inheritance alongside ofstream or as a contained object replacing the character array. The problem here is that stringstream is a relatively recent addition to the standard library, and my compiler currently supports only strstream, which is a poor substitute. For now, I leave the stringstream upgrade as an exercise for the interested reader.
The setDisplay method allows the programmer to specify a function for displaying error messages to the user. setDisplay takes a pointer to a function that has the same signature as the following:
void displayErr(const char *msg);You should normally implement the displayErr function (or whatever you choose to call it) in the same module as main. You should then pass the function to Errstream, using setDisplay, as early as possible within main (or at least before the program can generate any errors you might want to display with Errstream). The beauty of this scheme is that it disconnects the user interface used by the error display method from the recording of the error itself. You can sprinkle error messages liberally throughout your code, yet you need decide only once, at the start, how to display them.
The logOnly methods are used to read or set a flag that tells Errstream whether to display errors to the user. If this flag is set to true, Errstream writes errors to the log file but does not display them to the user. This might be useful for recording information that is not of immediate interest to the user. A third logOnly function is declared outside the Errstream class at the end of Listing 1. This is an iostream manipulator used for setting the logOnly flag via stream << syntax. Errstream resets the logOnly flag to false each time it writes a message, so the programmer must set logOnly for each message individually as desired.
In addition to the error string itself, Errstream logs the source code file name and line number that generated the error. The dspl macro defined near the beginning of Listing 1 accomplishes this task, by passing the predefined macros __FILE__ and __LINE__ to the Errstream manipulator functions setDebugFile and setDebugLine. The dspl macro then uses the displayError manipulator (not to be confused with displayErr mentioned above) to record and display the error message. Errstream declares the displayError manipulator as a static member just so dspl can use it in this manner. The result of all of this (whew!) is that dspl can be used at the end of an Errstream output statement, much as endl is used for iostreams. The following snippet using the global Errstream object errout (discussed later) shows an example use of dspl:
errout << "Oops" << dspl;The global setDebugFile and setDebugLine functions use the Errstream file and line methods (indirectly) to set the source code file and line numbers displayed by dspl. They do this via some convoluted iostream shenanigans that I discuss in more detail in the next section.
If a given error message is encountered two or more times in succession, by default it is entered in the log file only once, along with the repetition count. This helps prevent excessive growth of the log file. The programmer can turn this behavior on and off and check its status using the collect methods.
The purpose for the diffLevel methods and the differentiate manipulator is somewhat obscure, and I have found only occasional uses for them, so I do not discuss them here. The interested reader can find the documentation for these methods in the online source code. The test driver shown in Listing 3 shows a sample usage of differentiate, from which you can infer its necessity.
There is normally no need to create any new instances of the Errstream class. Simply include the header file Errstream.h to gain access to the global instance errout.
Extending iostream
Creating a new class based on iostream mostly consists of writing new inserter and/or extractor operators and manipulators. An inserter writes to the stream, while an extractor reads from the stream. A manipulator is a special function used with the << operator notation to change the characteristics of the stream.
Inserters and extractors are easy simply overload the << and >> operators respectively. Parameterless manipulators (such as logOnly and endl) are also easy. These non-member functions take a non-const reference to a stream, modify it in some way, and return it. If a manipulator must operate directly on member data, it should be declared as a friend.
Things get a lot trickier when creating a manipulator that takes a parameter (such as differentiate, setDebugFile, and setprecision). You must first write a helper class (such as EMANIP_int and EMANIP_string in Listing 1) to hold the parameter, along with a pointer to a manipulator-like wrapper function that also takes a parameter of the desired type. The parameterized manipulator function itself (e.g. differentiate in Listing 2) returns an object of this new type (e.g. EMANIP_int) constructed using the supplied parameter value, and a pointer to a wrapper function (e.g. set_diffLevel in Listing 2). The wrapper function does the actual modification to the supplied stream and then returns it. The final piece of the puzzle is an inserter operator declared and implemented as a friend inside the helper class (EMANIP_int). This is the operator that the compiler actually uses when it encounters the parameterized manipulator (e.g. differentiate). The purpose of this inserter is to call the wrapper function with the supplied parameter and return a reference to the modified stream.
Usage Example
Listing 3 shows a small test driver that illustrates the usage and most of the features of Errstream. The displayErrMsg function at the top of the listing simply prepends the word "ERROR:" to the display of each message. Alternate implementations might instead display an MS Windows or X/Motif error dialog. After setting the error display function using Errstream::setDisplay, the program exercises the inserters for the various data types, using the default log file. Then the program opens a new log file, exercises the logOnly manipulator and verifies the mechanism for collection of multiple identical messages in the log file. Finally, the program illustrates the usage (and necessity) of the differentiate manipulator. The screen and log file output of the driver program is shown in Figure 1.
Conclusion
Errstream provides a simple, standard method for logging and displaying errors and/or debug information. I use it in all my code, from test drivers and quick experiments, to large-scale applications. The design could be improved by replacing the internal fixed-length character array with a stringstream from the standard library when implementations of that class are more widely available.
Reference
[1] Scott Meyers. Effective C++: 50 Specific Ways to Improve Your Programs and Designs (Addison-Wesley, 1998), pp. 17-19. ISBN: 0201924889.
Brad Offer has worked for Boeing in Kent, Washington for the last 12 years, most recently in the Defense & Space Group writing software for data collection, reduction, and analysis; and operator control of an airborne infrared sensor. Before that he was an officer on a nuclear fast-attack submarine stationed in Hawaii. He has been programming for 14 years, including the last eight years in C++.