Contents
Overview
This document acts as a manual. For requirements and further implementation details see Exception (Requirements). Exception handling is a mechanism designed to handle the occurrence of exceptions, special conditions that change the normal flow of program execution.
Different error handling strategies
No error Handling
Advantages
Disadvantages
- No error handling at all is a very bad solution of error handling
Error handling with return values
Advantages
- At least there is some error handling
Disadvantages
- No need to handle the error. This can result in aftereffects when the caller ignores the error.
- No way to signal errors at construction time. An additional "init" or "isInitialized" method is needed.
- If not implemented in a consequent way, the type of the error will be lost in a call stack
if (callFunc() != OK)
return NOT_OK;
- The place of origin of the error will be lost in a call stack.
- The return value cannot be used to return the REAL result, instead a by-reference parameter must be used
Error handling with exceptions
Advantages
- Separates application code from error handling code
- Need to handle the error somewhere or Application will terminate
- Exceptions can be thrown in constructors (no need to use an "isOpen", or "isInitialized" method)
- Type of exception is preserved
- Place of origin is preserved and can be located using the call stack
- Exceptions use stack unwinding (local objects are destructed and their destructors are called)
Disadvantages
- Throwing exceptions over threads is not supported natively
Using exceptions
Exceptions can be thrown using the MIRA_THROW macro. You should always use this macro instead of the throw
keyword, since the macro adds additional diagnostic information about the source file name and line number where the exception was raised:
string readString(istream& stream)
{
...
if (stream.fail() )
}
The macro takes two parameters, where the first parameter specifies the exception class. The second parameter is a human-readable string that describes the error that has happened.
To add additional information to that error string you can use the << stream operator:
MIRA_THROW(XRuntime,
"Critical error in device '" << deviceName <<
"', OS error code: " << lasterror());
Usually, you won't need to handle each exception since they will be handled by an upper instance, e.g. the caller that calls your method. In these cases you let them simply pass through your code:
string myMethod(istream& stream)
{
...
Parser parser;
...
string name = readString(stream);
string value = readString(stream);
parser.parse(name, value);
...
}
If an exception is thrown within one of these three calls in the above example, the processing of your method will be aborted and all local objects (e.g. the parser-Object) will be destructed cleanly. In this way, the stack is unwinded upwards until a caller handles the exception. If none of the callers handles the exception the application will terminate with an error message.
To handle an exception you need to surround the code portion that might throw an exception with a try-catch block:
void readFile(const char* filename)
{
string value;
try
{
ifstream stream(filename);
value = myMethod(stream);
}
catch(XIO& ex)
{
value = "Default Value";
}
...
}
Exceptions MUST be caught by-reference to avoid performance penalties and to enable all features of our exception system.
A catch-block only catches exceptions of the specified class or derived classes. To catch different types of exception you can add multiple catch-blocks. To catch all exceptions regardless of their type use catch(...)
:
try
{
...
}
catch(XRuntime& ex)
{
...
}
catch(XLogical& ex)
{
...
}
catch(std::exception& ex)
{
}
catch(...)
{
...
}
Instead of using catch-blocks to fully handle an exception you may use them to do some cleanup or to bring your program into a determined state. Afterwards you can "rethrow" the exception by using the throw;
keyword:
MyClass* myObject = new MyClass;
try
{
...
}
catch(...)
{
delete myObject;
throw;
}
Instead of using the throw
keyword you can also use the MIRA_RETHROW macro. It allows you to add additional diagnostic information, that was not available to the code that produced the exception but might help the user to identify the problem:
string readString(istream& stream)
{
...
if (stream.fail() )
}
string readXmlStream(istream& stream)
{
try
{
...
value = readString(stream);
}
catch(Exception& ex)
{
MIRA_RETHROW(ex,
"... in XML tag <" << lastTag <<
"> at line: " << currentLine);
}
}
void readFile(const char* filename)
{
ifstream s(filename);
try
{
readXmlStream(s);
}
catch(XIO& e)
{
}
}
If an exception is thrown in the above example, the following error string will be generated:
Invalid format
... in XML tag <parameter> at line: 12
... in file: ~/mytest.xml
This error string will be shown to the user if the exception is never catched and the application is terminated. Additionally, this string can be obtained by calling the what() method of the std::exception (which is also implemented by mira::Exception). Beside the error string the mira::Exception also keeps track of the callstack which can be returned using the callStack() method:
try
{
...
}
catch(Exception& ex)
{
cout << "Exception: \n" << ex.what() << endl;
cout << "Stack: \n" << ex.callStack() << endl;
}
catch(std::exception& ex)
{
cout << "Exception: " << ex.what() << endl;
}
The output generated in the above example may look like this:
Exception:
Invalid format
... in XML tag <parameter> at line: 12
... in file: ~/mytest.xml
Stack:
#1 0xb6a004cf in readString(istream&)
at /home/test/ExceptionExample.C:13
#2 0xb7fd7dcd in readXmlStream(istream&)
at /home/test/ExceptionExample.C:24
#3 0xb6c30165 in readFile(istream&)
at /home/test/ExceptionExample.C:37
There are already several exception classes defined in the Exceptions.h header. The type of the thrown exception should follow some criteria:
- Exceptions that can only be detected at runtime and can not be avoided in the design phase or at compile time are called runtime exceptions. These should use the XRuntime as base type.
- Exceptions that can be detected at compile time or in the design phase are called logical exceptions. These should use the XLogical as base type. In a bug-free and well tested application only runtime exceptions should occur if any.
You can add your own exception types when needed.
Guidelines
General
There are several tips one should keep in mind when using exceptions:
- Don't LOG an error and throw an exception. Do only one thing.
- Don't throw an Exception (base type). Use derived types instead.
- Don't catch std::exception, (...) or Exception. Catch derived specific types instead.
- Don't create too many different derived types of exceptions. Use existing ones.
- Don't catch and throw - you will lose the stack trace. Use MIRA_RETHROW instead.
- Don't catch and ignore. Handle only exceptions you CAN handle.
When should I use exceptions?
An exception is by name any situation where something exceptional and unexpected happens that was not foreseen by the designer or programmer. There is a very fine line between exceptional and expected. After all, one can say: "Whenever a programmer checks for an error the error was expected somehow!" This is why there is no general guideline for "when to use exceptions". Nonetheless some hints will be given:
- Whenever you have a not yet implemented/supported function in your interface throw an exception instead of returning NULL or false.
- Whenever you LOG an error and return NULL you might think of using exceptions INSTEAD.
- Whenever a function is called with invalid (e.g. out of bounds) parameters a logical exception could be thrown.
- Whenever access to system functions, drivers or devices results in an error a runtime exception could be thrown.
- Whenever parsing/interpreting data or reading data from a device/file results in an error a runtime exception could be thrown.
Exceptions can also be used to avoid ugly de-initialization statements on return.
void init()
{
FILE* f = NULL;
HANDLE* driver = NULL;
char* memory = new char[20000];
DriverSession* session = NULL;
f = openHugeFile("test.bin");
if ( !f )
{
delete[] memory;
return;
}
if ( !openDeviceDriver(myDevice, &driver) )
{
delete[] memory;
close(f);
return;
}
if ( !initDevice(driver) )
{
delete[] memory;
close(f);
closeDriver(driver);
return;
}
if ( !openSession(device, &session) )
{
delete[] memory;
close(f);
closeSession(session);
closeDriver(driver);
return;
}
if ( !startSession(session) )
{
delete[] memory;
close(f);
closeSession(session);
closeDriver(driver);
return;
}
}
void init()
{
FILE* f = NULL;
HANDLE* driver = NULL;
char* memory = new char[20000];
try
{
f = openHugeFile("test.bin");
if ( !f )
if ( !openDeviceDriver(myDevice, &driver) )
if ( !initDevice(driver) )
if ( !openSession(session) )
if ( !startSession(session) )
}
catch(XMy& ex)
{
delete[] memory;
close(f);
closeDriver(driver);
closeSession(session);
return;
}
}
When should I avoid exceptions
As for the question "when should I use exceptions" there are no general guidelines for this question. But again some hints to follow:
- Whenever a function is designed in a way the caller uses or expects an exception as a result this is called expectation handling and should be avoided.
class Container
{
void containsItem(const Item& i)
{
if ( mSet.count(i) == 0 )
MIRA_THROW(XUnknownItem,
"Container does not contain item");
}
protected:
std::set<Item> mData;
}
...
Container myContainer;
...
try
{
myContainer.containsItem(Item("item1"));
}
catch(XUnknownItem& ex)
{
}
- Throwing and handling exceptions in time critical code could affect performance in a negative way because of the stack unwinding
How should I handle exceptions?
Often the best way to deal with exceptions is to not handle them at all. If you can let them pass through your code and allow destructors to handle cleanup, your code will be cleaner.