The C++ Standard Library Part 1
Language Features
ExceptionsOne problem in programming is how to signal and handle error conditions. One method is to use return values of functions to signal errors. However, one problem with this is that if the code calling the function doesn't know how to handle the error, it needs to propagate the error somewhere else. Another problem is that there are some places that error codes can't be used. Constructors, for instance, have no return value. To address these issues, C++ has exceptions. [11] Exceptions are one of the many topics in C++ that are at the same time both simple and hideously complicated. The concept itself is fairly simple. Instead of returning an error code when something goes wrong, you can instead throw an exception. The syntax to do so is correspondingly simple. void * ptr = some_function_returning_a_pointer(); if (!ptr) throw SomeExceptionType("Out of memory"); This causes the function to stop execution and start destroying everything on the stack until it finds something that can handle the exception. try { function_that_had_the_previous_code(); } catch (const SomeExceptionType & ex) { std::cerr << ex.what() << std::endl; } This code essentially says: I'm about to execute code that might throw an exception (the try block). If an exception is thrown, then I'll handle exceptions that I know about, in this case SomeExceptionType. If I get an exception that I know how to deal with, execute the code in the catch block. You can throw almost any C++ type, like character literals, integers, or class types. This is one of the instances in C++ that you shouldn't take advantage of the flexibility given to you. Throwing character literals, integers, floating point numbers or other primitive types is bad practice. When throwing exceptions you should throw exceptions of class types, and throw them by value. throw SomeExceptionType("Some Error String"); // good SomeExceptionType stack_variable("Some Error String"); throw &stack_variable; // bad; throwing local variable address throw new SomeExceptionType("Some Error String"); // bad; throwing newly allocated object throw "Some Error String"; // bad; throwing a string literal When you catch an exception you should catch by reference. [12] catch (SomeExceptionType & e) // good catch (SomeExceptionType e) // bad; catching by value It's also possible to have multiple catch blocks for different exception types and it's possible to have an catch block that will catch any exceptions thrown in the try block. try { // some code that might throw exceptions } catch(SomeExceptionType & e) { // handles exceptions of type SomeExceptionType } catch(...) { // handles any other exceptions } The catch blocks are tried in order. So having catch(...) before any other catch block makes the other catch blocks pointless. If you catch multiple types of exceptions then the exception types should come in the order of most derived exceptions before the more general exceptions. class DerivedType : public BaseType { // stuff }; // good try { } catch (DerivedType & e) { } catch (BaseType & e) { } catch (...) { } // bad try { } catch (BaseType & e) { } catch (DerivedType & e) { // will never catch anything since BaseType handler will // handle DerivedTypes too } catch (...) { } catch (SomeOtherType & e) { // will never catch anything since (...) traps all exceptions } Of course, knowing how to write a catch block is not very useful unless you know what to put in the catch block. In general, one of two things will be true. Either you don't know how to handle the exception, but you need to clean up resource or you have an idea what the problem is and how to handle it. In the second case you'll generally be the one who threw the exception, so you know how best to deal with it. For example, if you throw an out of memory condition, you might disable optional features and try to re-run the operation. (But see set_new_handler() later in this series.) But first, let's see what I mean by cleaning up resources. So when an exception occurs, the application goes into a meltdown mode. First, it takes the exception thrown, and copies the exception into a safe area. Then the stack unwinds, which means that everything on the stack is destroyed in the reverse order of their creation. void function_that_throws(void) { SomeObject obj1; throw MyException("Whoops."); } void function_with_try(void) { SomeObject obj2; try { SomeObject obj3; SomeObject * obj4 = new SomeObject(); function_that_throws(); SomeObject obj5; } catch (MyException & e) { std::cerr << e.what() << std::endl; } } In the above code, when the exception is thrown, obj1 is destroyed, then obj3 is destroyed. obj5 hasn't been created yet, so will not be destroyed. The object pointed to by obj4 will not be destroyed since it is not on the stack. obj2 won't be destroyed because it is outside the try/catch blocks. It would be destroyed if, instead of a MyException, some other exception was thrown, since the catch block wouldn't handle that. In any case, this code demonstrates a memory leak. The object pointed to by obj4 is not deleted, and there's no way to get at the object since all pointers to it are lost when the exception is thrown. So one way to write non-leaking code when you dynamically allocate something via new is to do something like: SomeObject * obj = new SomeObject(); try { // do stuff that might throw an exception delete obj; } catch (...) { delete obj; throw; } // done with obj, continue doing stuff There are some things to note about this code. The first thing is that in the catch(...) block, there's a throw all by itself. This rethrows the same object that was caught by the catch block. In this case, I've handled the exception by cleaning up the obj pointer, and I don't know how to actually deal with this error, so I'm going to punt and hope that some enclosing catch block knows how to deal with things. The other thing to notice is that this is really ugly code. Keep in mind that destructors of objects are called when the stack is unwound. So if we instead had a class that held the pointer instead of just a normal pointer, and the class destroyed the pointer automatically, we wouldn't need the try/catch blocks. The standard library does supply such a class, called std::auto_ptr<>. std::auto_ptr<SomeObject> obj(new SomeObject()); // do stuff that might throw an exception obj->some_function(); obj.reset(); This code creates a new SomeObject and gives ownership of the object to a std::auto_ptr<SomeObject>. If there's an exception, the auto_ptr will have it's destructor called, which in turn will call the delete on the pointer it was given. You can use operator -> on the auto_ptr to get at the members of the object just like a normal pointer, and the auto_ptr has member functions of its own like reset(), which deletes the object held by the auto_ptr. std::auto_ptr<> and classes like it are called smart pointers; they act like pointers, but have a little more intelligence built in. However, as smart pointers go, std::auto_ptr<> isn't very smart because all it can do is call delete. For example, you can't use it to hold dynamic arrays allocated with operator new[], because you should use operator delete[] instead of operator delete with dynamic arrays. Instead, of trying to use std::auto_ptr<> with dynamic arrays, you should use a std::vector<> (which I cover in the next article). You also can't use std::auto_ptr<> with something like the FILE * returned by C file I/O function fopen(), since you need to call fclose() and not delete. In this case, the C++ standard library has the classes, std::fstream, std::ofstream and std::ifstream. I'll cover all these classes in more detail later, but the general concept embodied by all these classes are the same: an object acquires a resource when it is created, and handles releasing the resource when it is destroyed. This technique is referred to resource acquisition is initialization or RAII. RAII is a very useful technique in C++ and not just for exception safety reasons. You can also specify a function will not throw an exception. [13] // will not throw any exception void function(void) throw() { // stuff } Generally, however, you will only want to do this if you are trying to implement a function or interface that requires a no-throw function, such as some custom allocator functions. Exceptions are the primary method the C++ standard library uses to signal error conditions and deal with error conditions from your code, and the C++ language itself will throw exceptions in various situations. As such the standard library includes a set of standard exceptions. The <exception> header defines the exceptions std::exception, which is the base class for all exceptions in the standard library, and std::bad_exception, which is an exception class used with exception specifications. The <typeinfo> header defines std::bad_cast, which is thrown if a dynamic_cast on reference types fails, and std::bad_typeid which is thrown if a typeid operation fails (such as when passed a null pointer). The <new> header contains std::bad_alloc which is thrown if operator new or operator new[] fails. Finally, <stdexcept> defines a number of exception classes, some of which are thrown by standard library classes or functions. std::logic_error std::domain_error std::invalid_argument std::length_error std::out_of_range std::runtime_error std::range_error std::overflow_error std::underflow_error Generally, it's good practice to derive your own exception classes from the standard library exception classes, and in particular from one of the classes in <stdexcept>. std::logic_error and its children std::domain_error, std::invalid_argument, std::length_error and std::out_of_range are used for errors on the part of the programmer. For example, supplying an index that is out of the range of valid indices. Often these situations are instead handled by assertions. However, they can be useful, for example, when interfacing with scripting languages. std::runtime_error and its children, std::range_error, std::overflow_error and std::underflow_error are useful when dealing with potential problems that are not necessarily the result of programmer error. |
|