Introduction
Exceptions in C++ are very frequently explained through the syntax elements which in essence do not explain thoroughly how it all works. Here, we will create some examples to explain how it really works and what options a developer has.
Background
If you take another syntax element, like the for or while loop, it is almost always very clearly explained what is going on and how a developer should use it, what the possible pitfalls are, etc. However, for some strange reason, I could not find a clear and concise explanation on exceptions that goes down to the same depth. There are several hidden penalties, there are even more possibilities within exceptions which you should be aware of.
This article does not teach you what the best way or best practice is on its own. This is better a collection of examples explained through general efficiency that will help you decide what works best for you in shaping a stable system of exceptions in your code.
The main purpose of the exception mechanism is to:
- provide a safe exit from an unusual but planned situation
- give information about the type of the unusual event
- offer more detailed information about each event
- set back the program execution point to a safer location which is less affected by the event
The code samples
We will use a step by step approach. First, let us see a general exception class or structure details.
class ExcTest
{
private:
int info;
public:
ExcTest()
{
}
ExcTest(int nInfo) {
info = nInfo;
}
ExcTest(ExcTest& exc)
{
}
void ChangeInfo(int nInfo) {
info = nInfo;
}
~ExcTest() {
}
};
In normal circumstances, any information field should be hidden as private or protected. Use the empty constructor like ExcTest() above if you want to throw an empty exception which would hold only the information about the exception type (read below about other ways of doing the same). Use the constructor to create an informative exception. In normal circumstances, the access members to the information fields, like ChangeInfo above, should not be the principal way of setting the information about an exception.
Basic type example
try
{
throw 5;
}
catch (int i)
{
}
We are catching the basic type, a copy of a variable will be used as it would with any other function call that has (int i).
try
{
throw 5;
}
catch (const int& i)
{
}
This is more efficient since we are using a reference and claim it as constant (if i is changed, we can remove const and still get the benefit of a reference usage at least).
Throwing by value
try
{
ExcTest exc; throw exc; }
catch (ExcTest epass) {
}
With this (not that rare) usage, the same exception is copied twice, created three times, before it is caught. Some information about the exception could be lost depending on the process of copying. Observe that each time we mention copying, a copy constructor of the exception type, class, or structure must be accessible.
try
{
throw ExcTest(123); }
catch (const ExcTest& epass) {
}
With this usage, the same exception is not copied at all before it is caught. No information about the exception is lost. The requirement is that we need a constructor that will be able to record the information about the exception. (You might find yourself in a strange situation that you have to expose the copy constructor in the class of an exception, although you have no intention of using it. This proves that the compiler is scratching the head on exceptions.)
Double catch
try
{
try
{
throw ExcTest(123); }
catch (ExcTest& epass) {
throw epass; }
}
catch (const ExcTest& epass) {
}
With this usage, the same exception is still copied once before it is caught. Some information about the exception could be lost.
try
{
try
{
throw ExcTest(123); }
catch (const ExcTest& epass) {
throw; }
}
catch (const ExcTest& epass)
{
};
Now, we are dealing with the same exception as the original one all the time.
Throwing by pointer
try
{
ExcTest* exc = new ExcTest(123);
throw exc; }
catch (ExcTest* epass) {
delete epass; }
This is a correct usage, but we have to deal with new and delete, which can throw an exception on their own. (Imagine we are catching an out of memory exception.)
try
{
throw new ExcTest(123); }
catch (const ExcTest* const epass) {
delete const_cast<ExcTest*>(epass); }
This is a complete, although excessive, picture of how we can create a precise catch using pointer.
Exotic usages that shed more light on the subject
try
{
ExcTest exc; throw (ExcTest*)&exc; }
catch (ExcTest*)
{
}
As you can notice, we did not pass a variable name, it is only catch(ExcTest*). This is because a variable passed would be a zombie, previously destroyed one, however we still have the information about the exception type (pay attention that some compiler exception mechanisms would make the previous exception, although destroyed, still available and even readable, but using it would be equal to playing with fire).
try
{
throw (ExcTest*)NULL; }
catch (ExcTest*)
{
}
We did not pass a variable name since it would be NULL, however we have information about the exception type. Knowing that this is possible, it is not bad to always check an exception pointer for NULL.
Global static usage
try
{
static ExcTest exc; exc.ChangeInfo(132); throw (ExcTest*)&exc; }
catch (const ExcTest* const epass) {
}
If we use a static exception, the same exception is always used in this scope, and although we are dealing with pointers, there is nothing to delete. Of course, multithreading can create some problems, but not always critical ones, since most of the time we are interested in the exception type.
Usage within function
void rFunct()
{
throw ExcTest(123); }
void lFunct()
{
try
{
rFunct();
}
catch (const ExcTest& epass)
{
throw; }
}
void tFunct()
{
try
{
lFunct();
}
catch (const ExcTest& epass) {
}
}
If we are always as precise as given in this example, the throw-catch can be as efficient as
having a global static exception object.
Non-basic type exceptions
To complete the story, we will add that we can use any data type for an exception. Here is an example of using a pointer to a function as the exception type.
int sampleFunc(int i)
{
i+=2;
return i;
}
void tryFunct()
{
try
{
throw &sampleFunc;
}
catch(int (*calc)(int))
{
int t = calc(5);
}
}
It is always up to you to decide which benefits of a type you want. Pointer to a function gives: static memory access, type information, no additional information about the exception, and some additional functionality since we are passing a function. This and similar ideas can produce some innovative exception designs in your program.
Conclusion
- Every throw by value creates a copy of the exception object unless we follow a strict syntax of
throw [anonymous instance]; and later just throw;.
If we miss that, deeply nested catches can quite lose their performance on the way out and we can lose information about the exception. Of course, additionally, we should always catch a value by constant reference (unless we need to change the exception object when we can use only a reference).
- Throwing by pointer is possible and has a more clear syntax, you can create a variable anywhere; however, the problem is that we are dealing with an exception while we are asking for memory juggle on the heap, which can throw an exception of its own, and the second problem is that we have to delete the exception object somewhere later. A smart pointer could simplify that, but deleting is still there just hidden. Unless planned in advance, the code within a
catch block should not be able to throw an exception.
- Throwing by a reference converted into the pointer is an efficient way, but we would safely know only the exception type. For that purpose we can use the cast of
NULL equally well. If we throw or catch just an exception type, the exception variable passed within a catch block remains anonymous.
- Throwing by a static reference with the cast into a pointer is very efficient, providing that we calculate in advance the size of all exception objects used that way, since they are created on the global stack. However, if we compare this last with the properly treated case 1., they are almost equally efficient. Still, if a module has a set of typical and small number of exceptions thrown frequently, creating them as static might not be a bad idea. You can create a global set of exceptions on the heap as well.
Any choice made should fit well with those exceptions that are thrown internally by library functions.
Points of interest
It looks like the exception system is somewhat clumsily designed regarding the required syntax. If you use constant reference for an exception within catch, throw an anonymous instance and later throw again the same exception using only throw; the entire scope of chained catch blocks appears shared, regardless of the local function's, module's, or other scopes. If you miss any of these, the shared scope is suddenly cut into individual areas where an exception is copied and recreated again and again. On the other hand, knowing that catch areas can be easily shared among totally unrelated functions or modules (in essence, we can create a local variable that has a temporary global scope) offers some new insights into the possible architectural solutions.
Now that you have, we hope, a clearer picture of how it really works, you can use the method that suits your needs best. However, if you follow the set of rules:
- create an informative exception only through the constructor
throw [anonymous instance];
- catch by constant reference
- throw again the exception using just
throw;
most of the problems about exception usage should be resolved. Using pointers, however, does put some new design moments into perspective.
Corresponding design pattern
Additionally, you can try to protect the code against undesirable effects of the syntax peculiarities through the design of the class for exceptions. For example, you can remove or make a default constructor private or protected, or make the copy constructor private or protected. That way a compiler will complain about less efficient syntax immediately. (Some older compilers will object to hiding the copy constructor in an exception type, even though it is not used. Do not be surprised if throw can access your copy constructor even when it is set private. Exceptions are, as we explained, not completely charted read: compiler tested territory). If you want to use pointers, make adequate adjustments.
template<typename e_Info>
class BaseException
{
protected: e_Info info;
BaseException()
{
}
BaseException(BaseException& exc)
{
}
public:
BaseException(e_Info qInfo)
{
info = qInfo;
}
~BaseException()
{
}
};
History
- Sunday, October 16 2011, 11:15 AM, first version.
- Tuesday, October 21 2011, 12:45 AM, general template design added.