Logical Operators Professional Software Development & Computer Consultation 707-A Dunbar Avenue P.O. Box 815 Dunbar WV 25064-0815 Telephone: (304)768-5079 THE ALERTDRIVER C++ CLASS LIBRARY EXCERPTS FROM USER'S GUIDE Version 1.00 References to businesses, organizations, and individuals within this publication are supplied only for informational purposes and in no way imply or infer endorsement or recommendation of the products and/or services of Logical Operators or its associates or affiliates unless explicitly stated. All brand names, trademarks, and registered trademarks appearing throughout this document are properties of their respective owners. The AlertDriver Class Library source code, object code, and all associated documentation are copyrighted 1994 by Logical Operators. All Rights Reserved. Part number ADUG940125. Preface At Logical Operators, we encountered many problems, portability issues, and maintenance issues when writing error-reporting code for the reusable libraries used in our applications. Nested if-then-else code detracted from the clarity of algorithms, hard-coded error messages caused reusability problems, and re-routing messages from the screen to other devices (such as printers) was difficult to say the least. Our solution to these problems, the AlertDriver Class Library, avoids all of these problems while providing a new software library with modularity, encapsulation, a consistent call interface, and efficient code size and speed. The AlertDriver Class Library is reusable and extensible to multiple application environments and hardware configurations. And perhaps most importantly, this easy-to-use class library provides the flexibility of multiple error-reporting methods within the same program while including the capability to easily enable, disable, or change reporting methods at both compile-time and run-time! Chapter 2 AlertDriver Class Library Concepts More Than Just Errors The AlertDriver Class Library does more than just simple error-reporting, it alerts the user to virtually any program condition during execution (hence the name). There are several types of program conditions, or alerts, which you may wish to present to the user during program execution: errors, information, messages, and warnings. Errors, the most serious types of alerts, occur when a program detects a state which makes continuation of the current branch of execution impossible. In interactive applications, an error usually stops program execution until acknowledged by the user. A typical error might be: "Cannot find file C:\YOURFILE.DAT." Information alerts occur whenever an application informs the user of a program condition or the occurrence of an event. Information alerts do not reflect execution problems and exist only for the convenience of the user. In interactive applications, an information alert stops program execution until acknowledged by the user. A typical information alert might be: "File C:\YOURFILE.DAT was successfully loaded." Messages also occur when the application informs the user of a program condition. Like information alerts, messages do not reflect execution problems and exist only for the convenience of the user. Unlike information alerts, messages do not stop program execution in interactive applications. Once posted, a message remains active until replaced by another message or cleared by the application. A sample message might be: "Loading file C:\YOURFILE.DAT..." Warnings (sometimes referred to as confirmations) are reported when the application prompts the user for a decision. Warnings are typically posed as a question with three possible answers: YES, NO, or CANCEL. Warnings pause program execution until the user makes a decision, then continue program execution based upon that decision. A common warning is "Do you want to overwrite file C:\YOURFILE.DAT?" Although the AlertDriver Class Library provides the means to deal with all of these types of alerts, this chapter will concentrate only on error handling to simplify the discussion. Please keep in mind that the concepts of error handling also apply to each of the other types of alerts. The AlertableObject Concept In most class libraries, the majority of classes are ultimately descendants of a single root class (usually named something like Object and having little actual functionality). Most new classes created by users of the library inherit the properties of an existing class, even if the class is as basic as Object. Thus, most objects (or class instantiations) in an application can be referenced and treated as a lowest common denominator - an Object. Now let's suppose that the root class has the ability to report errors it encounters during method executions. If such a class exists, it becomes easy to derive more specialized classes while simply inheriting the ability to report errors. Our programs can invoke the member functions of an object, knowing that if an error occurs, the object itself will report the problem to the user! Our programs only need to be aware of the success or failure of the member function's execution. If such a capability could be added to the root class of a commercial application framework, then the majority of the framework's classes (and their descendants) would be capable of reporting their errors to the user in a standardized manner right out of the box without any additional programming! Of course, not every programmer would have access to the framework's source code to implement this, and we may not want to add additional overhead to such a rudimentary class anyway. So in the true spirit of object- oriented programming, we would design a new class which directly inherits the capabilities of the framework's Object counterpart and adds some basic error-handling capabilities (see Example 4). Example 4 - A sample class declaration which could add error- handling capabilities to the base class (Object) of an application framework. The AlertDriver class library creates an error-handling class in a similar manner. class AlertableObject : public Object { /*add member functions below to enable error handling*/ ... }; //class AlertableObject The AlertDriver Class Library includes a class similar to that in Example 4 and implements the class in such a way that it can either be used without a class hierarchy or integrated into practically any C++ class library: new or existing, commercial or private. We have named this class AlertableObject: it provides the functionality to alert the user of errors. By serving as a base class, AlertableObject provides a common calling interface to our error-handling member functions. In addition, because AlertableObject contains a public member function interface, our error- handling routines are accessible from program code throughout the application. Benefiting From AlertableObject By making AlertableObject a direct descendant of a framework's root class in the class hierarchy, error- handling capabilities can easily be inherited by your classes and their descendants. Of course, classes which are derived from the root class (and bypass AlertableObject, like the existing classes in the framework) will not inherit the error-handling capabilities. So what other benefits exist from using the AlertableObject class in an application? Suppose we have a code module which has just invoked a member function of a descendant of AlertableObject. Our calling module does not report any errors which occur within the object's member function (that's the object's responsibility). However, our calling module may be interested in determining if the member function was able to execute successfully, and if not, why. This simple scenario is an excellent reason to have an object's member functions return error codes. It is easy to declare a group of constants which represent error conditions for each class (see Example 5). Not only does this practice enforce good programming by removing hard- coded values from the member functions and calling modules, but a wide range of values can be represented within the code returned by each member function. In addition, returned error codes remove the need for an unnecessary global variable (such as errno) or class data member to store the most recent result of a method execution. Example 5 - Error constants can easily be created and returned from AlertableObjects. const long int NOERR = 0; const long int FILENOTFOUND = 1; const long int OUTOFMEMORY = 2; ... class MyObj : public AlertableObject { public: virtual long int MemFunc1(void); //returns error code ... }; //class MyObj By testing the returned error codes, the module calling such member functions can smoothly recover from errors occurring within the object while avoiding the "matching-else" syndrome for error reporting (see Example 6). A pleasant side-effect is that the source code becomes much more readable and understandable. Example 6 - The same algorithm used in Example 1 is implemented here, using calls to an object's member functions rather than "plain" function calls. The object MyObj is derived from class AlertableObject, allowing MyObj to report errors to the user when they occur. Notice how much more readable the code below appears than the code of Example 1. ... if (MyObj.func1() == NOERR) if (MyObj.func2() == NOERR) if (MyObj.func3() == NOERR) ... Perhaps the biggest benefit of the AlertableObject, however, lies in its encapsulation. Because the object is self- contained and able to report its own errors in a platform- independent manner, it can be linked into multiple applications, none of which has to deal with reporting the errors which could occur internally. Imagine how much easier your next programming project would be if you could simply plug some of your existing classes together and forget about handling the errors, confident that they will be handled automatically! With the AlertDriver Class Library, you can! Introducing The AlertDriver The AlertDriver is the class in the hierarchy which provides the platform- and hardware-dependent error-reporting capabilities to objects derived from AlertableObject. Each object derived from AlertableObject uses an AlertDriver object to perform the actual reporting of each error message. The AlertDriver object determines how each error message is reported using the API (application programming interface) calls and hardware available to the application; hence, each AlertDriver object is an "error device driver" for the application environment. Because AlertDrivers are implemented as classes, any AlertDriver can be subclassed by the application programmer to change or control its specific actions. The AlertDriver class library supplies classes which can be used to report errors to the screen for interactive applications, or log errors to a printer or disk file for programs which normally run without human supervision. Additional AlertDrivers can be obtained or written for other platforms and/or hardware devices and linked into programs written for the AlertDriver Class Library with no change to the underlying application logic or AlertableObject source code. With the concepts described to this point and their inherent programming power and flexibility, most class libraries would stop here. However, the AlertDriver Class Library provides solutions to even more programming problems. An AlertDriver object itself can reference another AlertDriver object, effectively chaining, or linking, the AlertDrivers. By linking the AlertDrivers for an AlertableObject, the AlertableObject can report a single error in multiple ways (for example, an error can be reported to the screen and logged in a disk file). AlertDrivers can be attached and detached from AlertableObjects in any combination at either compile-time or run-time for maximum program efficiency and flexibility. Each AlertDriver can also have its actions controlled at either compile-time or run-time via a set of processing flags. Error-handling, for example, can be temporarily turned on or off for each AlertDriver without the need to disconnect or destroy the driver. Other useful processing flags, which allow a tremendous amount of control over the AlertDriver during run-time, are also available. The AlertDriver library also mirrors the concepts of client- server technology: each AlertableObject can be considered to be a client, each AlertDriver a server. The client initiates an action (i.e.: report an error message) and each server has the ability to perform that action. Multiple clients can be attached to a single server and a client may be served by multiple servers. The ability to connect several AlertableObjects to the same AlertDriver allows applications to retain all of the advantages previously discussed while minimizing the amount of memory overhead needed to implement those features. For example, in a spreadsheet application, each spreadsheet object could can be derived from an AlertableObject. If each cell required its own AlertDriver, then available memory would decrease rapidly as the spreadsheet grew. However, by allowing each cell to share a common AlertDriver, the memory needed to implement AlertDriver processing is held to a small, fixed amount (the amount needed to allocate the common AlertDriver). Since all cells in a spreadsheet normally report errors in the same manner anyway, the unnecessary allocation of duplicate AlertDriver objects is avoided. In the next chapter, we will discuss how easily your programs can use the AlertDriver Class Library as we step through a tutorial. Chapter 3 An AlertDriver Tutorial Creating New Objects To use the AlertDriver Class Library, you must include the following line in your source code file: #include The ALERTOBJ.H header file contains the declaration for the AlertableObject class. (The implementation of the AlertableObject class is supplied in a separate object code file which is platform-dependent - see the Compiling & Linking chapter for details on compiling and linking your code with the supported compilers/platforms.) To create classes of your own, you simply derive your class from AlertableObject using public inheritance. (For purposes of demonstration, we will present a simple, and admittedly contrived, sample class named Integer, which by itself doesn't really do anything useful, but will allow us to demonstrate the proper syntax and implementation of the AlertDriver library concepts - sample platform-independent source code implementing Integer is included with the AlertDriver Class Library.) Your class will automatically inherit the member functions and data of AlertableObject (see Example 7). Example 7 - To derive your own classes, derive your class from AlertableObject using public inheritance. class Integer : public AlertableObject { ... /*declare class-specific member functions and data members here*/ ... }; //class Integer Programmer-Defined Codes, Detection, & Reporting Once you have inherited the capabilities of the AlertableObject class, you will want to access that functionality from your member functions. There are two basic ways of using the AlertableObject member functions to alert the user of program conditions: the "quick-and-dirty" method and the "preferred" method. The Quick-And-Dirty Reporting Method The quick-and-dirty method of using the AlertDriver Class Library is usually used during program testing and/or debugging. Using this method, your object passes a literal string to the attached AlertDriver to alert the user of the program's condition. The quick-and-dirty method retains the platform-independent output of the AlertDriver, but the messages themselves are hard-coded into the program, clearly not the most elegant solution. However, this method is the quickest and easiest to use for quickly getting a test program to work or for porting existing programs to the AlertDriver Class Library. Programs written using the quick- and-dirty method can easily be ported to the preferred method later. To use the quick-and-dirty method, your code calls one of the Report*() member functions. There are four such functions (see the Class Library Reference chapter for details), one for each type of alert: ReportError(), ReportInfo(), ReportMessage(), and ReportWarning(). (ClearMessage(), a related function used in conjunction with member function ReportMessage(), clears the last message posted.) Example 8 shows how the code in Integer::TestAlertDriver() makes calls to these functions, passing literal strings to each; the AlertDriver for the Integer object reports those strings to the appropriate output device/platform. Example 8 - Calling the Report*() member functions inherited from AlertableObject. Each of these inherited functions passes the string along to the object's AlertDriver. void Integer::TestAlertDriver(void) { ... //test literal strings ReportMessage("Literal: This is a message string."); ReportError("Literal: This is an error string."); ReportWarning("Literal: This is a warning string.", adsYES); ClearMessage(); }; //member function Integer::TestAlertDriver One final note on reporting alerts: the Report*() function should always be called from the member function where the alert is first encountered or generated. By consistently following this suggestion, the code which calls your member functions can be written to assume that any alerts occurring within your member functions have already been reported by the time the function returns. The Preferred Reporting Method The preferred method of reporting alerts requires a bit more work than the quick-and-dirty method, but results in code which is infinitely more reusable and flexible. We highly suggest that any new programs and/or objects be written using the preferred method of alert reporting, as the preferred method results in programs which are better structured than programs using the quick-and-dirty method. Before we explain how to use the preferred method, we must first discuss the concepts of using programmer-defined codes for the alerts. Each unique alert for a class can be assigned a unique constant value for that type of alert within the class. For example, the error codes for the Integer object are numbered starting at one (1). Each unique warning alert for the Integer object is also represented with a code, with the first warning also being number one (1). The fact that both alerts have a code numbered one (1) does not cause a conflict - each type of code is used in its own context. Using constants (or #defines) to represent these codes in your program will help both you and other programmers to identify the different contexts when reviewing your code (see Example 9). While you may use negative numbers as alert code constants, do not use zero (0) as an identifier for an alert code - zero is pre-defined by the AlertDriver Class Library for each alert type as the constants errOK, infoOK, msgOK, and warnOK. Alert codes are represented by long integers. Example 9 - Define constants to uniquely identify each alert within a class. Each constant for a particular type of alert within a class may have the same value as the constant identifier of another alert type in the same class. Each constant may also be equal to constants of the same alert type in other classes. ... //define alert constants for class Integer const long errIntDivByZero = 1; const long errIntOverflow = 2; const long infoIntConstruct = 1; const long msgIntDestruct = 1; const long warnIntChange = 1; /*legal - different types of alerts*/ ... //define alert constants for class DiskFile const long errDiskFileNotFound = 1; //legal const long errDiskFileDiskFull = 1; /*illegal - same error code (1) defined twice for same class*/ ... Because errors are the most serious types of alerts, they should be returned from member functions whenever possible. This means each member function should return the error code of any error generated within that function, or return the error code returned by functions it calls. By adhering to this principle, the program module which calls your member functions will always receive an error code denoting the first error encountered during execution of your function. Of course, your functions should return errOK if no errors occur. The use of programmer-defined alert codes figures prominently in the preferred method of alert presentation. Rather than call the Report*() member functions, your member functions call the inherited Handle*() member functions. Each Handle*() member function corresponds to one of the Report*() member functions; the Handle*() member functions are: HandleError(), HandleInfo(), HandleMessage(), and HandleWarning(). Each of the Handle*() functions is passed an alert code as a parameter. This alert code is then passed to one of the Get*Text() member functions, converted to text, and the text is passed to one of the Report*() functions. Of course, there are four Get*Text() member functions: GetErrorText(), GetInfoText(), GetMessageText(), and GetWarningText(). To use the preferred method of alerting, your object must declare the appropriate alert constants for alerts which can be generated within your object. You then override the Get*Text() functions to convert those constants to strings. In a well-developed class hierarchy, if your Get*Text() function receives an alert constant which it cannot convert, it should call the overridden Get*Text() function, as the constant may represent an alert which was defined in a parent class. The original Get*Text() member functions in AlertableObject produce generic messages (i.e.: "Error 422 encountered.") for unrecognized alert codes. Example 10 shows the implementation of the GetErrorText() member function for the Integer class. Example 10 - Implementation of Integer::GetErrorText() member function. Notice how error codes which are unrecognized by this class are passed back to the overridden function. ... //define error constants for class Integer const long errIntDivByZero = 1; const long errIntOverflow = 2; ... class Integer : public AlertableObject { ... public: virtual void GetErrorText(const long errorCode, char *text); ... }; //class Integer ... void Integer::GetErrorText(const long errorCode, char *text) { switch (errorCode) { case errIntDivByZero : strcpy(text, "Divide by zero in Integer object."); break; case errIntOverflow : strcpy(text, "Value too large for Integer object."); break; default : AlertableObject::GetErrorText(errorCode, text); break; }; //switch statement to determine error text }; //member function Integer::GetErrorText To invoke the use of your strings in your member functions, you simply call the appropriate Handle*() member function, passing the correct alert constant. As always, you should call the appropriate Handle*() member function from the member function where the alert is first encountered. Because the Handle*() member functions automatically call the Get*Text() and Report*() member functions, a simple call to the appropriate Handle*() member function sets the entire chain of events into motion. At first, this may seem like a roundabout way of doing things, until you realize the amount of flexibility and reusability that your code gains. Because the Get*Text() member functions are virtual, you can override them in descendant classes. If you don't like the text for a specific alert code, you can derive a new class and return a different string for the code, even if you don't have the source code for the original class. The Get*Text() member functions don't even have to store the alert text in the program; the functions can be written to retrieve the alert text from an external text file. Using an external file to store the strings for each alert allows an application to use a minimal amount of program memory to store text. In addition, the text file can later be altered to update existing alert text without requiring recompilation of the application. Coupled with the platform-independent output of the AlertDriver, these capabilities give every AlertableObject maximum flexibility in presenting alerts to the user. Example 11 demonstrates how the overloaded "/" operator handles errant attempts to divide Integer objects by zero. Example 11 - The overloaded operator member function Integer::operator /() calls inherited member function HandleError() whenever it detects an attempted divide by zero. Invoking the HandleError() member function causes an eventual call to the Integer::GetErrorText() member function shown in Example 10. int Integer::operator / (int aValue) { int result; //holds the function result if (aValue) //specified value is not 0 result = val / aValue; else { HandleError(errIntDivByZero); result = INT_MAX; } return result; }; //overloaded operator Integer::operator / Changing & Sharing AlertDrivers All programs linked with the AlertDriver Class Library automatically gain access to a default AlertDriver referenced by the defaultAlertDriver pointer (defined in ALERTDRV.H). This global AlertDriver is usually an interactive, screen-oriented driver which provides alert reporting capabilities for the software environment for which the program was targeted. For example, defaultAlertDriver may provide text-mode output for a text- mode program, but will use Windows dialog boxes if the program has been compiled as a Windows application. Because defaultAlertDriver is available to be called from any part of your application, even your non-object code can call defaultAlertDriver's member functions (see Example 12). Calling one of the Handle*() member functions of the AlertDriver class is equivalent to calling one of the AlertableObject::Report*() member functions (see the Class Library Reference for details). Example 12 - Accessing defaultAlertDriver's member functions from non-object code. Calls made in this way are roughly equivalent to the quick-and-dirty method of reporting alerts from within objects. #include ... void main(void) { ... //test the defaultAlertDriver without an object defaultAlertDriver->HandleInfo( "Text from non-object code."); ... }; //function main() Each AlertableObject (and derived object) that is created by your program initially uses the defaultAlertDriver as its AlertDriver. By using this common AlertDriver, the AlertableObjects in your program gain all of the benefits of platform-independent error reporting with a minimum of code/data overhead. One AlertDriver controls the alerts of all your program's objects. There are times, however, when you may want to change this default behavior, particularly if the default AlertDriver does not provide the appropriate type of output for your object. For example, although the default AlertDriver is usually screen-oriented, your may want your object to log alerts to a disk file. You can easily change the type of AlertDriver used by your object by calling the AlertableObject::ChangeLinkedAlertDriver() member function. To call this inherited member function, you create a new AlertDriver object, then pass its address as a parameter to the function. Because all AlertDrivers should be allocated on the heap, this can usually be accomplished with one line of code. For example, the following line of code will change the AlertDriver for object MyObj to an AlertDriver which logs alerts to a text file named "myobj.log": MyObj.ChangeLinkedAlertDriver(new TextFileAlertDriver("myobj.log", radFREERESOURCE)); (The radFREERESOURCE constant forces the TextFileAlertDriver to free (close) the file whenever it is not in use; when the file is closed (not recording alerts), it can be accessed by other TextFileAlertDrivers or other applications. See the Class Library Reference for more details on controlling the TextFileAlertDriver.) AlertDrivers allocated by your application can serve several AlertableObjects at once, just as the default AlertDriver originally serves all of the objects in your program as they are created. Example 13 demonstrates how to share one newly allocated AlertDriver among two objects. Example 13 - Creating an AlertDriver object and sharing it among multiple objects. In this case, both objects will log their alerts to the same text file. Remember, AlertDrivers which you create within your program should always be allocated on the heap. ... AlertDriver *pAlertDriver; Integer a, b; ... /*log a and b alerts to the same text file - other AlertableObjects continue to use defaultAlertDriver*/ pAlertDriver = new TextFileAlertDriver("myobjs.log", radFREERESOURCE); a.ChangeLinkedAlertDriver(pAlertDriver); b.ChangeLinkedAlertDriver(pAlertDriver); ... To completely remove (not replace) the AlertDriver for an AlertableObject, call member function ChangeLinkedAlertDriver() with a parameter of NULL; doing this will remove all alerting capabilities for the AlertableObject. The AlertDriver Class Library defines the standard classes StdAlertDriver and TextFileAlertDriver for all software platforms and environments. StdAlertDriver uses the standard C++ iostreams cin, cout, and cerr to report alerts to the user while TextFileAlertDriver logs alert text to a file which is specified in its constructor. Additional AlertDriver classes may be defined for your specific software environment (for example, class TurboVisionAlertDriver is defined for programs using Borland's Turbo Vision class library). See the Class Library Reference chapter for full details on the AlertDriver classes which are available in your specific software environment. Experienced programmers know that one of the toughest parts of debugging a program is finding heap allocation errors. Because all AlertDrivers are allocated on the heap and may be shared among multiple AlertableObjects, you may be wondering if there is an easy way to determine when an AlertDriver is no longer being used and when it is safe to deallocate an AlertDriver. The answer is simple: you don't have to worry about deallocating any AlertDrivers, even those which you allocate! The AlertDriver Class Library handles it all for you! Each AlertDriver contains a count of the number of AlertableObjects using its services. This count is incremented each time an AlertDriver is "attached" to an AlertableObject. An AlertableObject is "detached" from an AlertDriver whenever the AlertableObject is deallocated, goes out of scope, or is attached to a different AlertDriver object through member function ChangeLinkedAlertDriver(); each time this happens, the attached AlertDriver's count is decremented. When the count reaches zero (the AlertDriver is no longer being used by any object), the AlertDriver is automatically shutdown and deallocated. What this means to you as a programmer is that you can allocate as many AlertDrivers of as many different classes as necessary and attach them to as many AlertableObjects as you desire with the understanding that each of the AlertDrivers will be deallocated as soon as it is no longer being used. Linking AlertDrivers Although you can change the AlertDriver used for each of your objects at any time, this still may not give you the level of flexibility you need. After all, it's nice to be able to switch between screen-based alerting and logging those alerts to a disk file, but what if you wanted to do both at the same time? With the AlertDriver Class Library, you can attach several AlertDrivers together in series so that the same alerts are reported in multiple ways. This process is known as "linking" the AlertDrivers. To link one AlertDriver to another, you must call the ChangeLinkedAlertDriver() member function of the first AlertDriver object. (The first AlertDriver object is defined as the AlertDriver which is attached to an AlertableObject, the remaining AlertDrivers are each attached to the previously linked AlertDriver. In effect, the AlertDrivers form a linked list, often referred to as a "chain.") A common use of linking AlertDrivers is to provide disk file logging in addition to screen alerting by linking a TextFileAlertDriver to an AlertableObject's AlertDriver. This can be most easily accomplished if the AlertableObject's AlertDriver is the default AlertDriver (see Example 14). Example 14 - By linking a TextFileAlertDriver to the default AlertDriver, all objects using the default AlertDriver will report their alerts to both the screen and a disk file (assuming that the default AlertDriver reports alerts to the screen). ... //attach a text file AlertDriver to the default driver defaultAlertDriver->ChangeLinkedAlertDriver( new TextFileAlertDriver("adtest.log", radFREERESOURCE)); ... It is important to note that when you call AlertableObject::ChangeLinkedAlertDriver() or AlertDriver::ChangeLinkedAlertDriver(), you are effectively replacing all AlertDrivers which are linked to the object/driver with the AlertDriver you specify. For illustration, assume that prior to calling the code in Example 14, defaultAlertDriver is the first of four (4) AlertDrivers which are linked together. After executing the code in Example 14, defaultAlertDriver will be the first of two (2) AlertDrivers in the link. The three AlertDrivers which were linked to defaultAlertDriver prior to the ChangeLinkedAlertDriver() call were detached from defaultAlertDriver (and possibly deallocated if they were not being used by other objects). This concept works in the other direction as well. You can link several AlertDrivers together, than pass the address of the first one to ChangeLinkedAlertDriver(); the first AlertDriver will be directly attached to the object/driver for which the call was made, while all other AlertDrivers in the link will be indirectly attached to the same object/driver. When several AlertDrivers are linked together, the first one processes the alert from the AlertableObject, then passes the alert to the next linked AlertDriver. This process continues until there are no more AlertDrivers in the chain. Because of this series of actions, it is suggested that you place only one screen-oriented AlertDriver in any AlertDriver chain. We also recommend that the screen- oriented AlertDriver (or any AlertDriver which interacts with the user) be placed first in the chain - this not only allows the user to be notified immediately of any alerts, but also ensures that other AlertDrivers in the chain are informed of the user's choice. For example, if an AlertDriver chain contained both a StdAlertDriver and a TextFileAlertDriver, you should place the StdAlertDriver first because it interacts with the user. In this case, warning alerts would be presented to the user first, then the user's choice would be recorded in the text file. Had the TextFileAlertDriver been placed first in the chain, recording the user's choice correctly in the text file would not be possible. Modifying AlertDriver Behavior At times, you may wish to temporarily disable processing of a certain type of alert or affect the output of an alert for an AlertableObject without changing or detaching the AlertDriver. Such changes can be made by altering the processing flags which are stored in each AlertDriver. Through the use of AlertableObject member functions GetAlertProcFlags() and ChangeAlertProcFlags(), you can obtain and alter the settings of the linked AlertDriver(s) for an object. (You can directly get/set the processing flags for an AlertDriver by calling its member functions GetProcFlags() and ChangeProcFlags() - see the Class Library Reference chapter for details and syntax.) The processing flags for an AlertDriver are represented as bit-mapped constants prefixed with the letters "adf" in the ALERTDRV.H header file. Four of these constants act as switches for processing the various types of alerts: adfERROR, adfINFO, adfMESSAGE, and adfWARNING. When one of these flags is enabled (on), the AlertDriver processes all alerts of that type which are passed to it. When a flag is disabled (off), the AlertDriver ignores alerts of that type, but still passes the alert to the next linked AlertDriver (if any). The return value of AlertableObject::GetAlertProcFlags() is an unsigned short which holds the value of all flags currently enabled for the first linked AlertDriver for the object. You can test to see if a flag is enabled (on) by performing a binary "AND" operation: //see if the first-linked AlertDriver for MyObj is currently processing warnings if (MyObj.GetAlertProcFlags() & adfWARNING) { //warnings are being processed for MyObj ... } Example 15 shows how to enable or disable processing for a specific type of alert using the AlertableObject::ChangeAlertProcFlags() member function; the second parameter determines whether the specified flags are enabled (cfoENABLE), disabled (cfoDISABLE), or intended to overwrite all of the current flags (cfoOVERWRITE) as well as whether the changes are made for the first AlertDriver (cfoDRIVER) or for all linked AlertDrivers (cfoCHAIN). Please see the Class Library Reference for more details. Example 15 - Enabling and disabling AlertDriver processing flags for an Integer object. Notice that when enabling or disabling specific flags, other flags for the AlertDriver are not altered. In this example, the first linked AlertDriver for object "a" is assumed to be a screen- oriented interactive AlertDriver; this is normally the case. ... Integer a; ... /*temporarily disable information alert processing to the screen for a*/ a.ChangeAlertProcFlags(adfINFO, cfoDRIVER | cfoDISABLE); /*now perform operations on a which are likely to generate information alerts*/ ... //enable information alert processing for a a.ChangeAlertProcFlags(adfINFO, cfoDRIVER | cfoENABLE); ... The adfALLALERTS flag is a combination of the adfERROR, adfINFO, adfMESSAGE, and adfWARNING flags. The adfTIMESTAMP flag controls whether an alert is timestamped when reported. When enabled, this flag causes the AlertDriver to generate a string holding the current date/time and appends the alert text to that string. The timestamp is formatted with the standard ANSI C function ctime() (see the Class Library Reference chapter for details.) A typical timestamped error message output through a TextFileAlertDriver might look like: Tue Dec 28 15:18:36 1993 ERROR - Literal: This is an error string. By this point, you've no doubt realized that a warning is the only type of alert which needs to return information to the program. When calling member function AlertableObject::HandleWarning(), you not only specify a string containing a question for the user, but you also indicate the default response to the question (please see the Class Library Reference chapter for details). If warning processing is enabled for the object's first linked AlertDriver, the question is presented to the user and the response is returned. However, this is one of only three possible states for the AlertDriver; warning processing may be disabled for the AlertDriver, or the object may not currently be attached to an AlertDriver. In the latter case, AlertableObject::HandleWarning() returns a constant indicating that no AlertDriver is attached to the object. But what should be returned in the former case? By default, if warning processing is disabled for the AlertDriver, the value returned is the default value you specified. Your program can assume that the user chose the default option and execution continues normally. However, there may be times when you want to know that the user was not given the opportunity to make a choice. In this case, you should enable processing flag adfDISCLOSURE for the AlertDriver. When adfDISCLOSURE is enabled and adfWARNING is disabled, calls to AlertableObject::HandleWarning() result in a special constant being returned; this constant indicates that processing for the current alert was disabled. Please see the discussion of AlertableObject::HandleWarning() in the Class Library Reference for full details of that member function's possible return values and how your code should deal with them. In most cases, the default settings of each AlertDriver's processing flags should be adequate for your application. Remember, when setting the processing flags for an AlertDriver which is shared between objects, you will be affecting the alert processing for all of those objects. What About Exception Handling? Because exception handling (a fairly new C++ language feature) is not supported by all C++ compilers (and not implemented consistently among those compilers which do support it), we will not attempt to show sample code for using this feature. However, this does not mean that the AlertDriver Class Library is incompatible with exception handling concepts. Exception handling allows for the separation of a program's error-generation code from the error-reporting/handling code. This is implemented through a try block (which contains a program's/routine's "normal" code) and one or more catch blocks (which handle the errors occurring within the try block.) Whenever a program exception (an exceptional condition, such as an error) occurs within the try block, the program executes a throw (raises the exception) and control is passed to one of the catch blocks. Once the exception reaches the catch block, the exception- handling code can be reported using an AlertDriver. Even if your objects have been written to throw exceptions, they themselves can catch the exceptions, report them using their linked AlertDriver(s), then re-throw the exceptions to higher-level code. If you plan to use exception handling in your C++ programs, we suggest that you use the AlertDriver Class Library as a environment- and device-independent means of reporting those exceptions. The concepts of the AlertDriver Class Library are a natural supplement to those of exception handling. Application Design Techniques - A Summary By now, you've read all about the power and flexibility of the AlertDriver Class Library. Compiled below are what we have found to be the most important coding and design techniques to use when writing applications for the AlertDriver Class Library. 1.Always invoke the appropriate handling for an alert in the member function in which the alert is first generated; this helps make your objects self-contained by allowing them to process their own alerts. 2.Whenever possible, return error codes from your member functions. This practice allows calling code to determine if your member functions were successful. 3.Use the preferred method of alerting whenever possible. This requires that your class be designed so that alert constants can be defined early in development, but rewards you with highly reusable code. By following this practice, the text for each alert can be stored externally and/or altered by descendant classes. 4.Use positive or negative numbers to represent alert constants, but never use zero (0). Zero should indicate that nothing is wrong. 5.Always try to share AlertDrivers among objects whenever possible, as this minimizes memory usage. However, you should give an object its own (non-shared) AlertDriver if you will be altering its processing flags; this practice will prevent the inadvertent processing changes of alerts from other objects. 6.Use the AlertDriver referenced by defaultAlertDriver for environment-independent alerting for non-object-oriented code. 7.Always create AlertDrivers on the heap. When they are no longer used, they will automatically be deallocated by the AlertableObject(s) which use them. 8.Link AlertDrivers whenever you need to provide alerting to multiple output devices. Linking can take place at compile-time or at run-time. 9.Modify an AlertDriver's processing flags whenever you need to enable or disable the output of certain types of alerts either temporarily or for the object's lifetime. Processing flags can be altered at compile-time or at run- time. Chapter 4 Compiling & Linking IMPORTANT: This chapter describes how to compile and link your programs to use the AlertDriver Class Library. You should read this section in its entirety before using the AlertDriver Class Library. Supported Compilers & Class Libraries As of the printing of this manual, the AlertDriver Class Library has been successfully tested with the following compilers/class libraries: Borland C++ v3.1 (text mode, Container Class Library, Turbo Vision, ObjectWindows) Microsoft C/C++ v7.0 (text mode) Other untested compilers/class libraries may be supported in text mode. You should read the supplied file named "README.1ST" on the distribution disks for the latest information which may describe features/updates added after the printing of this manual. Header Files There are four header files associated with the AlertDriver Class Library: ADWINDLL.H Prototypes of functions used in Microsoft Windows helper DLL. ALERTDRV.H AlertDriver class (and derivatives) declarations. ALERTOBJ.H AlertableObject class declaration. ENVIRON.H Compilation- and target-specific declarations. You will normally #include the ALERTOBJ.H and/or ALERTDRV.H header files in your source code files to give the compiler the necessary class declarations and/or function prototypes. (The Class Library Reference chapter lists the appropriate header file for each identifier which your program may need to access from the AlertDriver Class Library.) The ADWINDLL.H header file is automatically #included by the AlertDriver Class Library whenever you compile your program for the Microsoft Windows platform. You will normally not need to be concerned with this file. The ENVIRON.H header file is a special file which contains all of the compilation- and target-specific declarations needed to compile your program for different environments. You may need to replace the first two (2) #define statements in ENVIRON.H to compile your program for the appropriate environment (see below). Don't worry: this is extremely easy to do and the header file itself is clearly documented! Changing The Compilation Environment #define in ENVIRON.H The first #define statement in the ENVIRON.H header file defines the compilation environment and class library to be used. You should replace the #define with one of the valid entries listed below. Your choice of compilation environment will cause the compiler to make several assumptions (detailed below) about your code. The valid compilation environment #defines are: BORLAND_CONTAIN Borland's Container Class Library. OBJECT.H will be #included and the AlertDriver Class Library classes will be derived from Object. BORLAND_OWL Borland's ObjectWindows Library. Because Borland's Windows Custom Controls Library is used to present some alerts in customized dialog boxes, BWCC.H will be #included (BWCC.H is supplied with the Borland C++ compiler). OBJECT.H will also be #included and the AlertDriver Class Library classes will be derived from Object. BORLAND_TV Borland's Turbo Vision Class Library. #defines several Turbo Vision Uses_* preprocessor definitions, then #includes the TV.H header file. The AlertDriver Class Library classes will be derived from TObject. When you #define this compilation environment, you should #define your program's Turbo Vision Uses_* preprocessor directives before #including any of the AlertDriver Class Library header files. See the Turbo Vision documentation for more details. NOROOT The AlertDriver Class Library classes will be compiled without a common root class. Changing The Target Environment #define In ENVIRON.H The second #define statement in the ENVIRON.H header file defines the target environment for the executable file to be produced. You should replace the #define with one of the valid entries listed below. Your choice of target environment will cause the compiler to make several assumptions (detailed below) about your code. The valid target environment #defines are: TARGET_DOS The program will run under the Microsoft DOS (or compatible) operating system. TARGET_WINDOWS The program will run under the Microsoft Windows (or compatible) operating environment. Header file ADWINDLL.H is #included (this indirectly #includes WINDOWS.H). When you #define this target environment, you should #define your program's WINDOWS.H preprocessor directives (if any) before #including any of the AlertDriver Class Library header files. Of course, not every combination of compilation environment and target environment is valid, so make sure that the combination you choose makes sense (if it doesn't, the compiler will usually give you an error). For example, the #defines shown here are illegal: #define BORLAND_OWL //use ObjectWindows Library #define TARGET_DOS //ILLEGAL - cannot compile OWL apps as DOS apps The correct settings for ENVIRON.H for this example would be: #define BORLAND_OWL //use ObjectWindows Library #define TARGET_WINDOWS //OWL apps must be Windows apps Linking Your Programs To link your programs, you must link the appropriate library to your application in your MAKE or PROJECT file (see your compiler manual for details on linking). Refer to the lists below for the appropriate library for your compilation environment, target environment, and memory model. IMPORTANT - Make sure you link your programs to the right library. If you link the wrong library, your program will not execute. If you #defined TARGET_DOS in ENVIRON.H as your target environment: Link with one of the following libraries, whichever is appropriate for the setting you #defined for the compilation environment in ENVIRON.H and your program's memory model: ADBRDCCx.LIBBorland C++ Container Classes - There are six of these files, where x = the first letter of the memory model (tiny, small, medium, compact, large, or huge). For example, ADBRDCCL.LIB is for use with large memory model programs. ADBRDNRx.LIBBorland C++, no root class - There are six of these files, where x = the first letter of the memory model (tiny, small, medium, compact, large, or huge). For example, ADBRDNRL.LIB is for use with large memory model programs. ADBRDTVL.LIBBorland C++ Turbo Vision - large memory model. ADMSDNRx.LIBMicrosoft C/C++, no root class - There are four of these files, where x = the first letter of the memory model (small, medium, compact, or large). For example, ADMSDNRL.LIB is for use with large memory model programs. If you #defined BORLAND_TV as the compilation environment, link this file: TV.LIB Borland's Turbo Vision library (supplied with the Borland C++ compiler). If you #defined TARGET_WINDOWS in ENVIRON.H as your target environment: Link with one of the following libraries, whichever is appropriate for the setting you #defined for the compilation environment in ENVIRON.H and your program's memory model: ADBRWCCx.LIBBorland C++ Container Classes - There are four of these files, where x = the first letter of the memory model (small, medium, compact, or large). For example, ADBRWCCL.LIB is for use with large memory model programs. ADBRWNRx.LIBBorland C++, no root class - There are four of these files, where x = the first letter of the memory model (small, medium, compact, or large). For example, ADBRDNRL.LIB is for use with large memory model programs. ADBRWOWx.LIBBorland C++ ObjectWindows Library - There are four of these files, where x = the first letter of the memory model (small, medium, compact, or large). For example, ADBRWOWL.LIB is for use with large memory model programs. Also link this file: ADWINDLL.LIBImport library for the file ADWINDLL.DLL; make sure you distribute ADWINDLL.DLL with your finished application! If you #defined BORLAND_OWL as the compilation environment, link this file: BWCC.LIB Import library for Borland's Custom Control Library for Windows (supplied with the Borland C++ compiler); make sure you distribute BWCC.DLL with your application! Compiler-Specific Notes When writing programs which use the AlertDriver C++ Class Library, you may need to know how the supplied object code was compiled. The notes below indicate the compiler settings used to compile the AlertDriver Class Library. You may want to consider purchasing the Source Code Edition of the AlertDriver C++ Class Library if you want to recompile the AlertDriver Class Library with different compiler settings. Borland C++ v3.1 All object code libraries and DLLs were compiled assuming that all unsigned char variables should be explicitly defined (unsigned chars off). Stack checking was turned off. DOS libraries were compiled with 8086/8088 instructions. Windows libraries were compiled with 80286 instructions, target Windows 3.0 and above, and assume the use of the static versions of the standard, Container Class, and/or ObjectWindows libraries. Microsoft C/C++ v7.0 Stack checking was turned off and the libraries were compiled with 8086 instructions. Compiling The Sample Programs To compile any of the sample programs, you should follow all of the rules and suggestions previously mentioned in this chapter. Don't forget to correctly #define the compilation- and target-environment macros in ENVIRON.H! The sample file INTDEMO.CPP is the main file for the sample program using the Integer class. (The Integer class is discussed in the Tutorial Chapter.) To compile this program, you should compile the INTDEMO.CPP and INTEGER.CPP files, then link them with the appropriate AlertDriver Library .LIB file(s). These files can be compiled for any of the supported compilation- and/or target-environments. If you want to compile the INTDEMO.CPP sample program as a Windows application, you should also link the ADWIN.DEF module definition file to your executable. Note that the source code for the Integer class (in INTEGER.CPP), and the INTDEMO.CPP functions which manipulate the Integer objects, are totally compilation- and target- environment independent. Class Hierarchy In the listing below, derivative classes are indented underneath their parent class. For example, class WindowsAlertDriver is derived from class AlertDriver, which is derived from class AlertDriverLink. ROOTCLASS AlertDriverLink AlertableObject AlertDriver RecordingAlertDriver StreamAlertDriver TextFileAlertDriver StdAlertDriver TurboVisionAlertDriver WindowsAlertDriver BWCCAlertDriver Chapter 6 Creating Your Own AlertDrivers Design Concepts For most of your programming projects, the AlertDriver classes shipped with the AlertDriver Class Library will be sufficient for your needs. If, however, you would like to output alerts using operating-system API calls or hardware devices which are not supported by the standard AlertDriver Class Library, then you will need to create (subclass) your own AlertDriver classes. You may also want to subclass the supplied AlertDrivers to change their default behavior. In any case, this chapter presents the guidelines which should be followed while designing and writing new AlertDriver classes. Before designing your own custom AlertDriver class, you should first determine what type of AlertDriver the class will be. There are two basic types of AlertDriver classes: interactive (those which can interact with the user) and recording (those which provide output-only services). Interactive AlertDriver classes are derived from the AlertDriver class, while recording AlertDrivers are subclassed from the RecordingAlertDriver class. Because interactive AlertDrivers usually report alerts to the screen, they should be designed to use standard C, C++, or operating-system API calls whenever possible. Recording AlertDrivers, however, usually report to hardware devices (which are somewhat dependent on the operating-system) such as printers or RS-232C devices. Therefore, recording AlertDrivers often must be designed to use the API calls provided by the hardware device manufacturer. Requirements & Suggestions To write an AlertDriver class definition, you must have a good understanding of the API calls necessary to control the output resource, as well as an understanding of the class(es) from which you will be subclassing the AlertDriver. Of course, you must also have a C++ compiler for your target environment, as well as any prerequisites for the compiler. If you are planning to create your own AlertDriver classes, we recommend that you purchase the Source Code Edition of the AlertDriver Class Library. By examining the well- commented source code used to write the supplied AlertDriver classes, you can gain a much deeper understanding of the techniques used to write reusable AlertDrivers. Updating ENVIRON.H You will have to update the ENVIRON.H header file if you are trying to do one of the following: 1.port the AlertDriver Class Library to a new compilation environment (i.e.: use a currently-unsupported hierarchy class as the root class for the AlertDriver Class Library) 2.port the AlertDriver Class Library to a new target environment (i.e.: target your executable code for a currently-unsupported operating system) 3.redefine one of the macros specifying an AlertDriver system setting (i.e.: specify a different default AlertDriver or change the maximum size of an alert text string) If you need to update ENVIRON.H, we suggest you first print the file and study its heavily-commented preprocessor directives. Defining Your AlertDriver To define an AlertDriver, you must first plan to override the AlertDriver::Report*() member functions. Remember that for interactive AlertDrivers, member functions ReportError(), ReportInfo(), and ReportWarning() should halt program execution until acknowledged by the user. For recording AlertDrivers, program execution should never be halted. For full details on the intended actions of the Report*() member functions for both interactive and recording AlertDrivers, see the discussions of the AlertDriver::Report*() member functions in the Class Library Reference chapter. If you are writing a recording AlertDriver, you should call member function GetResourceControl() from your constructor(s), destructor, and Report*() member functions. If the resource control mode is radFREERESOURCE or radHOLDRESOURCE, your object should explicitly capture and free the resource when appropriate. (For the supplied TextFileAlertDriver, this is accomplished by opening/closing the output file.) Review the discussion of the RecordingAlertDriver data member resourceControl in the Class Library Reference for more details on controlling the output resource for a recording AlertDriver. You may also want to override the AlertDriver::GetTimestamp() member function to return the computer's current date/time in a format different from the default. Finally, if your AlertDriver encounters an error during output, you should not attempt to display an error message. Just clean up any allocated resources and return immediately. That's all there is to creating your own AlertDriver objects! If you have followed these guidelines, your AlertDriver objects should be usable by any AlertableObject without changes to the AlertableObject.