Errors Related to Exceptions
Partially Constructed Objects
When an exception prevents a constructor from running to completion, C++ will not call the destructor of the partially constructed object. Member objects, however, will still be properly destructed. This is not to say that exceptions are to be avoided in constructors. In fact, throwing an exception is usually the best way to indicate that a constructor has failed. The alternative is leaving around a broken object, which the caller must check for validity.
While you should often allow exceptions to propagate out of a constructor, it is important to remember that the destructor will never be called in such cases, so you are responsible for cleaning up the partially constructed object (e.g. by catching and then rethrowing the exception, using smart pointers, etc.).
Result: memory leak.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //--- class CtorThrow { public: CtorThrow(); ~CtorThrow(); //N.B. non-virtual, //not meant for subclassing. private: Base* first_p_; Base* second_p_; Derived member_obj_; //See the Training Wheels Class //for an explanation of these declarations. CtorThrow(const CtorThrow&); CtorThrow& operator=(const CtorThrow&); }; CtorThrow::CtorThrow() : first_p_(0), second_p_(0), member_obj_() { first_p_ = new Base; //Could also call a function/method that throws; //in any case, "first_p_" is leaked. throw SimpleString("Exception!"); second_p_ = new Base; } //Destructor will not be called when exception //leaves the constructor. CtorThrow::~CtorThrow() { cout << "Destroying CthorThrow" << endl; delete first_p_; delete second_p_; } //--- |
Exceptions Leaving a Destructor
If a function throws an exception, that exception may be caught by the caller, or the caller’s caller, etc. This flexibility is what makes error handling via exceptions into such a powerful tool. In order to propagate through your program, however, an exception needs to leave one scope after another ? almost as if one function after another were returning from a chain of nested of calls. This process is called stack unwinding .
As a propagating exception unwinds the stack, it encounters local objects. Just like in a return from a function, these local objects must be properly destroyed. This is not a problem unless an object’s destructor throws an exception. Because there is no general way to decide which exception to continue processing (the currently active one or the the new one thrown by the destructor), C++ simply calls the global terminate function. By default, terminate calls abort, which abruptly ends the program. In consequence, local objects are not properly destructed. While the operating system should reclaim the memory when your program terminates, any complex resource that requires your destructors to run (e.g., a database connection) will not be properly cleaned up.
Another consequence of an exception leaving a destructor is that the destructor itself does not finish its work. This could lead to memory leaks. Your destructor does not necessarily have to throw an exception itself for the problem to happen. It is far more common that something else called by the destructor throws the exception. In general, it is best if you make sure that exceptions never propagate out of your destructors under any circumstances (even if your compiler implements the Boolean uncaught_exception function, which can be used to test if an exception is already in progress).
Result: memory leak (destructor does not run to completion); local objects will not be properly destructed if another exception is active. Various resource leaks and state inconsistency are therefore possible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //--- //When DtorThrow throws an exception in in its //destructor, but no other exception is active, //all other objects are properly destroyed. try { cout << "Only one exception ..." << endl; Base obj1; Derived obj2; DtorThrow obj3; } catch(...) {//Catch everything. cout << "Caught an exception" << endl; } cout << endl; //When another exception is active, and DtorThrow //throws an exception in its destructor, the other //objects are *not* properly destroyed. try { cout << "Exception during another exception ..." << endl; Base obj1; Derived obj2; DtorThrow obj3; throw SimpleString("Exception!"); } catch (...) {//Catch everything; //we never get here. cout << "Caught an exception" << endl; } //--- |
Improper Throwing
When throwing exceptions, it is important to remember that the object being thrown is always copied. Hence, it is safe to throw local objects, but throwing dynamically allocated objects by value or by reference will cause them to be leaked. Copying is always based on the static type of the object, so if you have a base-type reference to an object of a derived class, and you decide to throw that reference, an object of the base type will be thrown. This is another variant of the object slicing problem covered earlier.
A more subtle slicing error occurs when rethrowing exceptions. If you want to rethrow the exact same object that you got in your catch clause, simply use throw; ? not something like throw arg;. The latter will construct a new copy of the object, which will slice off any derived parts of the original.
You also need to make sure that the copy constructor of the class that you are throwing will not cause dangling references. It is generally not recommended to throw exceptions by pointer; in these situations, only the pointer itself, rather than the actual object, is copied. Thus, throwing a pointer to a local object is never safe. On the other hand, throwing a non-local object by pointer raises the question of whether it needs to be deallocated or not, and what is responsible for the deallocation.
Result: object slicing, dangling references, and memory leaks are all possible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | //--- //Outer try block. try { //Inner try block. try { throw Derived(); } //Catch by reference -- won't slice. catch (Base& ex) { ex.example(); //O.K. //Rethrow ... throw ex; //Mistake -- slices! //Should just use "throw;". } //END Inner try block. } //Should be fine, but ... catch(Base& ex) { ex.example(); //... not what we expected! } //--- |
Improper Catching
Improper catching of exceptions can also lead to object slicing. As you might have guessed, catching by value will slice. The order of the catch clauses matters, too; always list the catch for an exception of a derived class before the catch of its base class. The exception mechanism uses the first catch clause that works, so listing base classes up front will always shadow the derived classes.
Result: object slicing; memory-related errors and other problems are possible if a base-class catch shadows a derived-class catch, thus preventing the latter from taking actions specific to a derived-type exception.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //--- try { throw Derived(); } //This not only shadows the Derived catch //clause -- it slices, too! catch (Base ex) { cout << "caught a Base" << endl; ex.example(); //Sliced! } //We never get here! catch (Derived ex) { cout << "caught a Derived" << endl; ex.example(); } //--- |
The Way Forward
This article has focused on finding the right role for C++ amongst today’s other popular languages, and on understanding its most difficult aspect: memory management. The tables of common memory-related errors presented here can be used as a handy reference, to find and avoid such errors in your own code. Subsequent articles of the series will continue to discuss C++ memory management in greater detail.
The second article will be devoted to describing the nature of the C++ memory management mechanism, so that you can begin to apply it creatively in your designs. After that, the third article will present a series of specific techniques that you can use as building blocks in your programs.
C++ memory management is an enormously useful tool for creating elegant software. Having gained a clear awareness of its dangers, you are now ready to understand its benefits. Enabling you to do so is, ultimately, the purpose of this series of articles.
By George Belotsky Initially published on Linux DevCenter (http://www.linuxdevcenter.com/)