Exception Management in C++ and Python Development: Planning for the Unexpected
- The Importance of Handling Exceptions
- A C++ Exception with No Handler
- A C++ Exception in a Call from Python
- Conclusion
- References
The Importance of Handling Exceptions
Exception handling is an area that programmers seem to either love or hate. I've even been surprised a few times by seeing code that swallows all exceptions, without exception! This is a lazy and hazardous way of implementing exception handlers, as illustrated in Listing 1.
Listing 1Swallowing all exceptions. Please don't do this at home—or at work.
try: aFile = open('anyOldFile.txt') myString = aFile.readline() print('File contents are: ') print(myString) except IOError as e: print "I/O error({0}): {1}".format(e.errno, e.strerror) except: print "Unexpected error:", sys.exc_info()[0] raise
Why might the code in Listing 1 be considered bad practice? Well, suppose some truly catastrophic exception occurs; for example, the runtime system runs out of memory. Listing 1 offers no way for the exception handler to cope in a graceful fashion. In other words, the last except clause in Listing 1 may be the wrong place to try to handle a more general exception.
Let's improve Listing 1 by simply removing the last except clause, as shown in Listing 2.
Listing 2No longer swallowing all exceptions (less is more).
try: aFile = open('anyOldFile.txt') myString = aFile.readline() print('File contents are: ') print(myString) except IOError as e: print "I/O error({0}): {1}".format(e.errno, e.strerror)
Listing 2 improves on Listing 1 by doing just one thing and doing it well; it handles the exception type IOError. If the code throws an IOError exception, the appropriate error details will be printed. An example is what occurs if the file in question doesn't exist:
I/O error(2): No such file or directory
What other exceptions might occur in Listing 2, apart from IOError? Well, any such exceptions must then be handled at a higher level; for example, in the code that calls Listing 2. On a deeper level, this can be considered an example of the Separation of Concerns design pattern. In this case, Listing 2 does some file I/O and handles any associated exceptions. Any other exceptions are the responsibility of the code that calls Listing 2. The exception-specific concerns have thereby been separated, as indicated in Listing 3.
Listing 3Our exception-handling hierarchy.
Listing 2 code -----------> Handles IOError only Listing 2 caller ----------> Handles all other exceptions
Viewed as a separation of concerns, the exception management strategy is distributed across the calling chain. The key requirement is to allocate only certain exception-handling responsibilities to certain code. This more sophisticated approach illustrates why the code in Listing 1 isn't very good, but is all too common.
Why isn't exception management more popular among programmers? Perhaps because it's not glamorous. Why think about possible errors in a completed piece of code, when you can just move onto the next coding task? Here's why: If you think a piece of code can't possibly fall foul of an exception state, you're probably wrong!
Writing robust exception-handlers is an opportunity to make your code stronger and more resilient. To illustrate this point, let's look again at Listing 2. Notice anything else missing from this listing? What about closing the file that we've just opened? The finally clause provides a nice mechanism for fulfilling this requirement. Let's do this with a little refactoring, as shown in Listing 4.
Listing 4Better handling of allocated resources.
aFile = None try: aFile = open('anyOldFile.txt') myString = aFile.readline() print('File contents are: ') print(myString) except IOError as e: print('I/O error({0}): {1}'.format(e.errno, e.strerror)) else: print 'No exception' finally: if (aFile != None): print('File is being closed') aFile.close() else: print('File already closed')
The extra lines of code at the end of Listing 4 provide a degree of enhanced robustness because we're giving back our allocated file resource. With the file closed in the finally clause, we guarantee this beneficial outcome, and we avoid the problem of our code leaving files open unnecessarily. We make our code far more robust by using a language feature, and we avoid the need for complicated and error-prone mechanisms such as state variables.
Running Python Example Code
One of Python's many merits is its lightweight nature. It's pretty straightforward to get started coding in Python—no need to install (at least initially) and learn to use complex IDEs such as Eclipse. Instead, you can simply run up a Python console. In Linux, this is as simple as running the python command, which results in something similar to Listing 5.
Listing 5The Python console.
Python 2.7.3 (default, Sep 26 2013, 20:08:41) [GCC 4.6.3] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> Here's how to get the Python host version: >>> import sys >>> sys.version '2.7.3 (default, Sep 26 2013, 20:08:41) \n[GCC 4.6.3]' >>>
Obviously, as code examples become more complex, copying-and-pasting the code into a console gets a bit clunky. But the console approach is useful for getting started or for testing code snippets.
So what about C++ exception management?
Handling Exceptions with C++
C++ also has a powerful exception-management facility. Listing 6 shows a simple example of C++ exception management. Here I attempt to force a memory-allocation exception called bad_alloc by allocating a pathologically large array of integers.
Listing 6C++ exception management.
#include <iostream> #include <exception> using namespace std; int main () { int* myarray = NULL; if (myarray) { cout << "Not NULL" << endl; } else { cout << "Is NULL" << endl; } try { int* myarray = new int[1000000000]; } catch (exception& e) { cout << "Standard exception: " << e.what() << endl; if (myarray) { cout << "Not NULL" << endl; } else { cout << "Is NULL" << endl; } } if(myarray) { cout << "Deleting myarray" << endl; delete [] myarray; } cout << "Returning" << endl; return 0; }
In Listing 6, I've also inserted some code to determine if and when the allocation occurs, via the call to new(). Listing 7 shows a run of the code where the exception occurs.
Listing 7A C++ exception handler in action.
Is NULL Standard exception: std::bad_alloc Is NULL Returning
Notice in Listing 7 that the exception handler has been invoked, and the type of exception is indeed bad_alloc. Naturally, this means that the runtime system was unable to provide the massive array I requested.
Of course, not everyone likes exception handlers. A veteran C++ programmer probably wouldn't use a try-catch block here. Instead, a pro would most likely just check that the call to new() returns a non-NULL pointer:
if(myarray) { // The myarray is not NULL if call to new() succeeded
Programming professionals also might dislike exception handlers because C++ exception checking potentially consumes valuable resources. For mere mortals, however, the use of the exception mechanism still has its merits. Aside from bad_alloc, the other exception types are as follows:
- bad_exception is thrown by certain dynamic exception-specifiers.
- bad_typeid is thrown by typeid.
- bad_function_call is thrown by an empty function object.
- bad_weak_ptr is thrown by shared_ptr when passed a bad weak_ptr.
Generic exceptions such as logic_error and runtime_error are another option, which programmers can use for application-specific purposes. Using runtime_error is potentially a good step in the direction of exception-centric development, where any exceptions get reported to a central repository.
Catching Exceptions by Reference in C++
The exception that can be caught by the handler in Listing 6 is a bad_alloc. The bad_alloc exception is itself derived from the standard base-class exception, so bad_alloc can be caught this way because it's part of the object graph. This works because capturing by reference in turn captures all related classes—which is good news because we can also capture other exceptions. If we capture an exception and don't know what to do with it, we can just rethrow it for handling at a higher level.
C++ and RAII (Resource Acquisition Is Initialization)
You might have noticed that the C++ example doesn't include the finally clause in its exception structure. The normal "finally" semantics are intended to be provided by a design feature of C++ called Resource Acquisition Is Initialization (RAII). Coupled with a feature called stack unwinding, RAII provides a model for exception-safe C++ code. RAII is a nice approach because it facilitates the exception-safe release of resources such as files, memory, allocated objects, and so on. RAII allows for appropriate destructor code to be invoked automatically as part of the exception management infrastructure.
RAII mechanisms are facilitated by some of the elements of C++ 11, specifically the smart pointer classes and mutex locks. Of course, the latter are geared toward multithreaded C++ programming; I described them to some extent in my article "C++ 11 Memory Management."