Exceptions as objects
The object-oriented design of C++ tends to treat the most possible entities as objects. Exceptions are not an exception to the rule. All classes which are thrown as exceptions by the OSE library, derive from the class OTC_Exception . To make a program portable to the same C++ compilers as OSE's, some of which do not support exceptions, it is important to derive any new exception class from the class OTC_Exception . Here is an abbreviated version of the definition of this class:
class OTC_Exception
{
public:
OTC_Exception(char const* theError=0);
// should be a string
// describing the error which has
// occurred. If is <0>
// then a description of
// <"Unknown error"> is saved. The
// description of the error will
// be displayed on the logger at
// priority level .
char const* error() const;
// Returns a description of the
// error which has occurred.
virtual void display(ostream& outs) const;
// Dumps a message which composes together
// all the information about the error on the
// stream . This should be redefined
// in derived class to first call the
// base class version of the function, ie.,
// , and then
// dump out any additional information which
// is kept in the derived class. The derived
// class should terminate each line of
// information with an and not use
// or .
protected:
virtual void throwIt()const;
// Must be redefined in a derived class. The
// derived implementation must contain the
// code .
};
A derived exception class must redefine some functions of this defintion. For example, it must override the throwIt() function. The implementation of this function in any derived class should contain:
#if defined(HAVE_EXCEPTIONS)
throw *this;
#endif
The reason of this rewriting is the inability of some compilers to deal with derivation mechanism. When an exception is thrown, the throwIt function used belongs to the most derived type in the exception hierarchy. If a derived class does not provide its own function, the exception is going to be thrown as a reference to the base class type. Some compilers truncate the class object and only use the base class type. Thus any information in derived classes is lost and exceptions can be caught only through the base class type.
Any derived classes should also redefine the display() member function to dump out to a stream a representation of the exception. As the OTC_Exception base class already dumps out some information, the display() function of a derived class should first call the base class version of this function.
To specify a specific exception class while throwing an exception, the function OTCLIB_THROW() must be used instead of throw() . OTCLIB_THROW() takes the exception class as argument. Here is an example throwing OTC_Exception :
if (theString == 0)
OTCLIB_THROW(OTC_Exception("Invalid input"));
or:
if (theString == 0)
{
OTC_Exception exception("Invalid input");
OTCLIB_THROW(exception);
}
In the case of a C++ compiler supporting exceptions, the OTCLIB_THROW() function will use the throwIt() member function described above, to raise a true C++ exception of the type specified to the function. In order to know what the exception was, any code which catches the exception should dump a representation of the exception to a stream by calling the display() member function. For example, to display a representation of the exception class to the message log facility, the catch clause would be written as:
catch (OTC_Exception& theException)
{
char theBuffer[2048];
OTC_LogStream theStream(theBuffer,sizeof(theBuffer));
theException.display(theStream);
theStream.flush();
...
}
Another possibility is to set the OTCLIB_LOGEXCEPTION environment variable before running the program. Doing this causes the OTCLIB_THROW() function to always display information about the exception via the logger whether or not it is caught. In the case of a C++ compiler which does not support exceptions, a representation of the exception is always automatically displayed via the logger regardless of whether the environment variable is set. It is followed by the call to the terminate() function supplied by the OSE library.
Preconditions
When using the classes in the OSE library, various preconditions exist which must be satisfied before calling any member functions of the class. If the preconditions of a function are not satisfied, the exception type OTCERR_PreconditionFailure is thrown. It is possible to generate a user-defined exception for failure of a precondition by using the macro OTCLIB_ENSURE() rather than exception class. For example:
#include
void function(char const* theString)
{
OTCLIB_ENSURE((theString != 0), "function() - Invalid input");
...
}
OTCLIB_ENSURE() macro is worth using because it automatically supplies information about the name of the file and line in the file where the macro is used.
The first argument to the OTCLIB_ENSURE() macro should be an expression which yields a non zero value if the condition is satisfied or, zero if the condition is not satisfied. The second argument to the macro should be a description explaining why the precondition failed.
Note: The macro OTCLIB_ENSURE_W() can be used in order to get the same style of error message as OTCLIB_ENSURE() generates and sends to the logger, but without stopping the application. It takes the same arguments but regards it as a warning and not an error and thus does not throw an exception.
Assertions
Assertions checks for the success or the failure of a condition are similar to the precondition checks. The difference between an assertion check and a precondition check is that an assertion check is generally used in development to check that the correctness of the program behaviour. Assertion checks are generally removed from a program at the final version. The exception type created in the event of a failed assert is OTCERR_AssertionFailure . An assertion check can be coded in the following manner using the OTCLIB_ASSERT() macro.
#include
void function(char const* theString)
{
OTCLIB_ASSERT(theString != 0);
...
}
An interesting feature of C++ is able to compile out the assertion checks out of the program when producing its final version. To activate this feature, the preprocessor symbol NDEBUG has to be defined. Because assertions can be compiled out of a program, the expression used in these assertions should not have any side effects on which the normal operation of the program relies.
General exceptions
An alternative solution to the precondition and assertion macros is the OTCLIB_EXCEPTION() macro. It can be used anywhere raising an error is needed. The only argument accepted by the macro is a string which describes the error so, any condition check must be performed explicitly. For example:
#include
void function(char const* theString)
{
if (theString == 0)
OTCLIB_EXCEPTION("function() - Invalid input");
...
}
Note: the type of the exception used is OTC_Exception.
Integration of user-defined exceptions
It is possible for the library to throw user-defined exception. The first step is to register a function to be called by the library when it needs to throw an exception. The supplied function must throw an exception using the user-defined exception mechanism and it must not return. If the function does return, the library calls the terminate() function.
To specify such a function, the user should call the function OTC_Exception::setThrow() at the start of the main() function passing it a pointer to your function. The exception function must take a single argument of the library exception which is being raised, and return 'void'. For example:
void throwMyException(OTC_Exception const& exception)
{
// throw an exception using home grown mechanism
}
main()
{
OTC_Exception::setThrow()(throwMyException);
...
}
Termination
The method used by most programmers to terminate a program written in C or C++ is to call the operating system exit() function. This will cause program termination, however by itself, this does not allow for any special actions to be undertaken in order to return the system to a stable state first.
Take for example, an application which places locks on files which it is using. If this were to encounter some unrecoverable error mid way through an operation, it would need to be able to remove those locks and release the files for use by other applications. The problem with using exit() directly is that knowledge of how to remove the locks is most likely not known at the point where exit() is being invoked.
A complimentary function to exit() is available on some systems. This is called on_exit() or atexit() , and allows the user to register a function which will be called when exit() is invoked. As this is not widespread, the terminate() and set_terminate() functions described previously, should be used instead when programming in C++. The use of these functions ensures that any user-defined cleanup actions will also be executed when an exception is thrown, but not caught, as this results in the 'terminate()' function being called.
The set_terminate() function provides a mechanism to register a function to be executed when terminate() is called, however, it is only possible to register one function. As it may needed to perform a number of actions when terminate() is called, an object based registration scheme for actions to run is supplied. This is implemented by the OTC_TObject class.
class OTC_TObject
{
public:
static void terminateAll();
// Iterates through all instances of
// this class and invokes cleanup()
// on each instance.
protected:
virtual void cleanup() = 0;
// Should be redefined in a derived
// class to perform any actions which
// need to be done in the event of
// abnormal program termination.
};
If any of the user-defined classes must do something special in the case of an abnormal termination of the program, these classes should be derived from OTC_TObject and, the cleanup() function should be defined. When one creates an instance of one class, it will be linked with any other classes, which also derive from the OTC_TObject class. The terminateAll() function, when called, will iterate through this list of classes and call the cleanup() function on each.
In order to invoke the terminateAll() function when the terminate() function is called, it is important to register the terminateAll() function. This is done by calling the set_terminate() function. Here is an example of code to illustrate it:
#include
main()
{
set_terminate(OTC_TObject::terminateAll);
...
}
And shown below is an example of a class which would be derived from the TObject class. In this example the cleanup() function ensures that the open file is closed and all data still in the stream is written out. If the file had a lock on it, the cleanup() could also have removed the lock. This is not shown in the example though.
class EX_Log : private OTC_TObject
{
public:
EX_Log(char const* aFile) : myFile(aFile) {}
private:
void cleanup() { myFile.close(); }
ofstream myFile;
};
It is recommended that the cleanup() function should do as little as necessary. It should not try and allocate any memory, as it may be because of memory exhaustion that the program is being terminated.
The example above shows the function terminateAll() being registered as the function to call when terminate() is invoked, normally however, this would not be done. The reason for this is that the terminateAll() function returns. If a function returns to the terminate() function from which it is called, the terminate() function will call abort() , which results in a core file being produced. Instead of registering terminateAll() , it is better to register a wrapper function which will call terminateAll() and then call exit() . For example:
void terminateApp()
{
OTC_TObject::terminateAll();
exit(1);
}
main()
{
set_terminate(terminateApp);
...
}
If the user wants to restart the program, rather than calling 'exit()', this program should be executed again.
A version of the terminateApp() function is supplied in the library. The function is called otclib_terminate_function() . As well as calling terminateAll() and exiting, the version of the function supplied in the library, will send a warning to the message log system indicating that the program is terminating. Here is the definition of this function:
void otclib_terminate_function()
{
OTC_TObject::terminateAll();
OTC_Logger::notify(OTCLIB_LOG_ALERT, "Program terminating");
exit(1);
}
To use this function, it is necessary to add the following call at the start of the program main() routine.
set_terminate(otclib_terminate_function);
Stack unwiding
When an exception occurs, and the stack unwound, only destructors for objects created on the stack are invoked. If an object had been created using operator new() and holding a pointer to that object via a stack variable, the object will not be destroyed, resulting in a memory leak. Three different classes are provided to assist in ensuring that objects allocated using operator new() or malloc() are destroyed when an exception occurs. These classes are OTC_Reaper , OTC_VecReaper and OTC_MallocReaper .
The concept behind these classes is that, after an object has been created on the heap, it is grabbed by an instance of one of the reaper classes. The reaper class is created on the stack. If an exception occurs, the destructor of the reaper class is called, resulting in the object on the heap being destroyed. This mechanism is only valid all over the code identified as possibly throwing exceptions and specified by the instance of the reaper class. After the point in this specific code, the reaper class is made to release the object on the heap. Provided the object is released, it will not be deleted via the reaper object in the case of an abnormal termination. Here is an illustration of this concept.
void function()
{
OTC_Reaper xxxObject;
Object* theObject = new Object;
OTCLIB_ASSERT(theObject != 0);
xxxObject.grab(theObject);
... code which could throw an exception
xxxObject.release();
... code which subsequently saves away object
... or deletes the object
}
The OTC_Reaper and OTC_VecReaper classes are templates and are used respectively for pointers to a single object, and pointers to an array of objects. The OTC_MallocReaper is not a template, and is used for memory which has been allocated using the malloc() function.