Some Useful Facts to Know Before Using C++ Exceptions
Exceptions can be tricky. Read on to get some tips and hints on how to handle exceptions in C++, and keep your software running smooth.
Join the DZone community and get the full member experience.
Join For FreeExceptions provide many benefits over error codes for error handling. Some of these benefits are:
- Exceptions cannot be silently ignored whereas checking the error code of a method can be ignored by the method caller.
- Exceptions propagate automatically over method boundaries, but error codes do not.
- Exception handling removes error handling and recovery from the main line of control flow that makes code more readable and maintainable.
- Exceptions are the best way to report errors from constructors and operators.
Despite these benefits, most people still do not prefer to use exceptions due to its overhead. Depending on the implementation, this overhead comes in two forms: time overhead (increased execution time) and space overhead (increased executable size and memory consumption). From these two, most are concerned about time overhead. However, in a good C++ exception implementation, unless an exception is thrown, no run-time overhead is incurred [2]. The real issue with C++ exceptions is not in their execution performance, but how to use them correctly. Following are some useful facts to know in order to use C++ exceptions correctly.
1. Exceptions Should Not Be Thrown From Destructors
Consider the following code:
try
{
MyClass1 Obj1;
Obj1.DosomeWork();
...
}
catch(std::exception & ex)
{
//do error handling
}
If an exception is thrown from the MyClass1:: DosomeWork()
method, before execution moves out from the try block, the destructor of obj1 needs to be called as obj1 is a properly constructed object. What if an exception is also thrown from the destructor of MyClass1
? This exception occurred while another exception was active. If an exception is thrown while another exception is active, the C++ behavior is to call the terminate()
method, which halts the execution of the application. Therefore, to avoid two exceptions being active at the same time, destructors must not throw exceptions.
2. Objects Thrown as Exceptions Are Always Copied
When an exception is thrown, a copy of the exception object always needs to be created as the original object goes out of the scope during the stack unwinding process. Therefore, the exception object’s copy constructor will always be called. C++ provides a copy constructor by default if we don’t provide one. But there may be cases where the default copy constructor doesn’t work; especially when the class members are pointers. If we use such objects as exception objects we should make sure that our exception classes have proper copy constructors. The most important thing about copying objects in C++ is that the copy constructor is always called based on the static type, not the dynamic type. Consider the following code [1].
class Widget { ... };
class SpecialWidget: public Widget { ... };
void passAndThrowWidget()
{
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget; // rw refers to a SpecialWidget
throw rw; // this throws an exception of type Widget!
}
In the above code, the throw
statement throws an object of type Widget
, rw
's static type is Widget
. This might not be the behavior that we want to execute.
3. Exceptions Should Be Caught by Reference
There are three ways to catch exceptions in the catch clause:
- Catch exception by value.
- Catch exception as a pointer.
- Catch exception by reference.
Catching exceptions by value is costly and it suffers from the slicing problem. It is costly because it needs to create two exception objects every time. When an exception is thrown, a copy of the exception object needs to be created no matter how we catch it, because when the stack unwinds the original object would go out the scope. If the exception was caught by value, another copy is made to pass it to the catch clause. Therefore, if the exception was caught by value, two exception objects are created, making the exception throwing process slower.
The slicing problem comes into effect when the catch clause is declared to catch a super class type and a derived class object is thrown as an exception. In that case, the catch clause only receives a copy of the super class object, which lacks the attributes of the original exception object. Therefore, catching exceptions by value must be avoided.
If exceptions were caught as pointers, the code would be as follows;
void doSomething()
{
try
{
someFunction(); // might throw an exception*
}
catch (exception* ex)
{ // catches the exception*;
... // no object is copied
}
}
In order to catch the exception as a pointer, the pointer to the exception object should be thrown and the throwing party must ensure that the exception object will be kept alive even after the stack unwinding, as a result of the throw. Even though a copy of the exception object will be created, it is a copy of the pointer in this case. Therefore, other measures should be taken to keep the exception object alive after the throw. This can be achieved by passing the pointer to a global or a static object, or creating the exception object in the heap.
Nevertheless, the user who catches the exception has no idea about how the exception object was created, and thus they are uncertain about whether to delete the receiving pointer at the catch clause or not. Therefore, catching the exception as a pointer is sub-optimal. Furthermore, all exceptions thrown from standard functions are objects, not pointers.
Catching exceptions by reference doesn’t have any of these issues that are observable in 'catch as a pointer' or 'catch by value.' The user doesn’t need to worry about the deletion of the exception object. No additional exception object will be copied, as a reference to the original exception object is passed. Furthermore, pass by reference doesn’t have the slicing problem. Therefore, the exception should be caught by reference for proper and efficient operation.
4. Prevent Resource Leaks in Case of Exceptions
Consider the following code:
void SomeFunction()
{
SimpleObject* pObj = new SimpleObject();
pObj->DoSomeWork();//could throw exceptions
delete pObj;
}
In this method new SimpleObject is created, then some work has been done through SimpleObject::DoSomeWork() method and finally the object is destroyed. But what if Object::DoSomeWork() threw an exception? In that case we don’t get a chance to delete the pObj. This introduces a memory leak. This is a simple example to illustrate that exceptions could lead to resource leaks and of course this can be eliminated by putting a simple try catch block. But in real life this scenario could happen from points of code where we can’t figure out the leak at first glance. One remedy for this type of cases is to use auto pointers from standard library (std::auto_ptr) [1].
5. When throwing an exception, do not leave the object in an inconsistent state
Consider the following example code [4]:
template <class T>
class Stack
{
unsigned nelems;
int top;
T* v;
public:
void push(T);
T pop();
Stack();
~Stack();
};
template <class T>
void Stack<T>::push(T element)
{
top++;
if( top == nelems-1 )
{
T* new_buffer = new (nothrow) T[nelems+=10];
if( new_buffer == 0 )
throw "out of memory";
for(int i = 0; i < top; i++)
new_buffer[i] = v[i];
delete [ ] v;
v = new_buffer;
}
v[top] = element;
}
If the exception “out of memory” was thrown, the Stack::push()
method leaves the Stack
object in an inconsistent state, because the top of the stack has been incremented without pushing any element. Of course, this code can be modified so that it won't happen. However, special care needs to be taken when throwing an exception so that the object which throws the exception will be in a valid state even after the exception.
Furthermore, this scenario happens frequently with mutexes or locks. In the following naïve implementation of ThreadSafeQueue::Pushback()
method, if an exception was thrown from the DoPushBack()
method, _mutex
will be kept locked, leaving the ThreadSafeQueue
object in an inconsistent state. To overcome this situation, lock_guards
can be used, as auto pointers are used to prevent memory leaks. It should be noted that lock_guard
has only been available in standard libraries since C++11, yet one can easily implement a lock_guard
class.
template <class T>
void ThreadSafeQueue::Pushback(T element)
{
_mutex.Lock();
DoPushBack(T);
_mutex.Unlock();
}
6. Exception Specification Should Be Used Carefully [1]
If a method throws an exception not listed in its exception specification, that fault is detected at runtime, and the special function unexpected()
is automatically invoked. The default behavior for unexpected is to call terminate()
, and the default behavior for terminate()
is to call abort()
. Therefore, the default behavior for a program with a violated exception specification is to halt. Consider the following code:
void f1(); // might throw anything
void f2() throw(int); //throws only int
void f2() throw(int)
{
...
f1(); // legal even though f1 might throw
// something besides an int
...
}
The above code is legal because this kind of situation might arise when integrating old code that lacks exception specification, with new code. But if f1()
throws something other than int
the program will terminate because f2() is not allowed throw anything other than int
. It is a bit hard to stop this problem from arising, but there are many ways to handle this if it does. Reference 1: Item 14 details remedies for this problem.
7. Exceptions Should Be Re-Thrown With Re-Throw
There are two ways to propagate a caught exception to callers. Consider the following two code blocks:
catch (Widget& w) // catch Widget exceptions
{
... // handle the exception
throw; // rethrow the exception so it continues to propagate
}
catch (Widget& w) // catch Widget exceptions
{
... // handle the exception
throw w; // propagate a copy of the
}
The only difference between these two blocks is that the first one re-throws the current exception, while the second one throws a new copy of the current exception. There are two problems with the second case. One is the performance cost because of the copy operation. The other thing is the slicing problem. If the exception object is a child type of Widget
, only the Widget
part of the exception object is re-thrown as the copy is always based on the static type.
8. Catch Clause for the Base Class Should Be Placed After the Catch Clause for the Derived Class
When an exception is thrown, catch clauses are matched in the order they appear in the code. An exception object’s type also matches with its super types in catch clauses, because child type is also a subset of super type. Therefore, when a child object’s exception is thrown, if a super type catch clause appears first in the code, that will be executed, even though the catch clause for the child type is there after the super type’s catch clause.
References:
[1] More Effective C++, by Scott Meyers, 1996.
[2] When and How to Use Exceptions, by Herb Sutter, 2004 (http://www.drdobbs.com/when-and-how-to-use-exceptions/184401836).
[3] Technical Report on C++ Performance, 2005 (http://www.stroustrup.com/performanceTR.pdf).
[4] Exception Handling: A False Sense of Security, by Tom Cargill (http://ptgmedia.pearsoncmg.com/images/020163371x/supplements/Exception_Handling_Article.html).
Opinions expressed by DZone contributors are their own.
Comments