Errors: Exceptions And C++

From Matt Morris Wiki
Jump to navigation Jump to search

Programming - Errors


These guidelines cover the basics.

Guarantees

In decreasing order of strength:

  • No-throw: Operations are guaranteed to succeed and satisfy all requirements even in exceptional situations. If an exception occurs, it will be handled internally and not observed by clients.
  • Strong: Commit or rollback semantics: Operations can fail, but failure will not have side effects (all data retain their original values).
  • Basic: No-leak guarantee: Partial execution of failed operations can cause side effects, but all invariants are preserved and no resources are leaked. Any stored data will contain valid values, even if they differ from what they were before the exception.
  • None: No guarantees are made.

"No-throw" may make demands of the classes used by the code. For instance, "no-throw" swap operations require that the members of the class in question also offer "no-throw" swap operations.

Resources And Robustness

Release Resources In Destructors, Not By Explicit Calls

C++ provides no automatic garbage collection, so we need a way to release resources without programmers having to remember to do it by hand. Luckily, C++ has a mechanism that makes this possible - whenever a variable goes out of scope, its destructor is called.

Some people call this "deterministic finalization", because it means that you know exactly when the destructor will run - it will run as soon as that variable goes out of scope. So if the destructor frees a resource, you know that the resource is getting freed. You don't need to worry about whether you're going out of scope via normal execution, or by an exception.

An example, assuming that "SmartPtr" deletes T in its destructor:

   if( enterBlock )
   {
      // Will delete memory when enclosing block is exited
      SmartPtr<T> refT = new T();
      
      func1(*refT);
      
      func2(*refT);
   }

We don't have to care if "func1()" throws an exception, or "func2()" does, or neither do. The pointer will be deleted whatever happens.

This point holds for any resource, memory, files, mutexes and so on. Never explictly call delete, or release any other type of resource. Let the destructor of a guard object going out of scope do this for you. Otherwise, if an exception exits your code before your explicit clean-up call, when will the resource ever be released?

By the way, this is why C++ doesn't need a "finally" keyword to ensure things get cleaned up when exceptions are thrown. If destructors always release resources, your C++ code can be written for the most part as if exceptions didn't exist.

Get A Good Reference-Counted Smart Template Pointer And Use It

If you are going to manage resources in C++, you will need to have a good smart pointer. Your smart pointer class will be widely used in your program, so it's worth spending a little thought on which one is best to use.

One important point: the "auto_ptr<>" smart pointer template in the standard is not the best choice here, despite being the only smart pointer in the standard library. This is partly because it has "non-intuitive copy semantics": that is to say, copying an auto_ptr transfers ownership of the held pointer from the original to the copy, which violates most people's expectations that copying leaves the source unchanged. It's also worth noting that, for this reason, auto_ptr wrappings can't be used as values in the standard library's container classes.

Try, instead, to make a reference-counted pointer your default smart pointer. The "boost" project's "shared_ptr" is a good place to start. So is the Scott Meyers pointer, if you make sure you've got the latest version. Or you could write your own, but be warned - they can be fiddly to get right.

One particularly nice property: if you have resources inside reference-counted smart pointers, and you don't require a copy to take a "deep copy" of everything, you can use the compiler-supplied copy constructor, assignment and destructor functions. This will completely remove the need to worry about exceptions for the majority of your classes, converting your bare resource into an object with the semantics of a "value", eg something you can use in standard library containers.

It is true that a reference-counted pointer is slower than an auto_ptr: it needs to allocate a shared area of memory for the instance count. But we're dealing with dynamically-allocated memory anyway, so the overhead in practice will not affect the majority of uses.

Acquire Resources In Your Constructors, Throwing Exceptions On Failure

Many C++ programmers are nervous about throwing exceptions in constructors, so it's very common for them to delay the major part of a complex object's construction to a later "init()" function. However, this does not follow commonly accepted good practice.

The creator of the C++ language, Bjarne Stroustrup, has put his views very clearly on this subject, in "Appendix E: Standard Library Exception Safety", from his book "The C++ Programming Language (Special Edition)". You can read the copy he keeps on his website.

His main argument is summed up in these two sentences: The two-phase construction approach leads to more complicated invariants and typically to less elegant, more error-prone, and harder-to-maintain code. Consequently, the language-supported 'constructor' approach should be preferred to the 'init()-function approach' whenever feasible.

Another way of putting it: life is far simpler if you know that an object's constructor will set it up fully. If you have to call some extra "init()" function before an object is "live", then things get a lot more complicated. Have you called that extra "init()" function yet? What if you call the "init()" function twice? What if your object is "const" - can you still call "init()" on it?

Although many C++ programmers are very hazy on it, the rule for what happens when an exception is thrown in a C++ constructor is in fact very simple. Any subobjects of the class being constructed that are already fully created have their destructors called, in reverse order of their creation. That's all. And since you will have written the destructors to free up resources (see above), then all allocated resources will be properly freed without the need for any extra work.

Never Throw Exceptions From A Destructor

If you throw an exception while another exception is being processed, the C++ standard dictates the "terminate()" function is called - this is not allowed to return to the main program. In other words, your application is about to die. So you don't want to throw exceptions while another exception is being processed.

When an exception is being processed, the call stack is rolled back, and lots of objects go out of scope. This means that their destructors are being called while the exception is pending. So, if you let an exception escape from a destructor, you're risking triggering that "terminate()" behaviour.

In practice, this is rarely a problem. The major use of destructors is to release resources, and releasing a resource that you have already acquired tends not to be the kind of operation that might throw an exception. But if a thrown exception is a possibility (for instance, you are logging object destruction to some network message sink that might not be available), make sure you use a catch(...) clause around the relevant parts.

Providing Information

Write An Exception Macro With File, Line And Function Information

You should always be able to tell where an error originated in your code.

Write a macro that will add the contents of the __LINE__, __FILE__, and __FUNCTION__ macros to your exception. If one of your platforms doesn't support __FUNCTION__, you can add an empty placeholder for it. This is, incidentally, one of few uses left for macros in the C++ language.

Use std::ostringstream to constuct error messages

Many C++ programmers don't seem to construct informative error messages. There is no particular reason for this, because the std::ostringstream class makes it very easy to write text that mixes strings, numbers and any other class that has stream output operations defined on it:

const Curve& CurveCache::getCurveForDate(
   const string& curveName,
   const MyDateClass& valuationDate)
{
   const Curve* p = 0;
   if( ! myCache.findValue(curveName, valuationDate, p) )
   {
      std::ostringstream oss;
      oss << "Could not find curve "
          << "name '" << curveName << "' "
          << "for date " << valuationDate;
      throwExceptionMacroAddingFileAndLine(oss.str().c_str());
   }
   return *p;
}

Write An Exception Class That Nests

Make it as easy as possible for people to rethrow an exception with added context, without losing the original exception's file, line and function information. The best way to do this is to allow one exception to contain ("wrap") another exception.

Further Reading

Stroustrup's 'Appendix E' On Standard Library Exception Safety

The guidelines on this page are perfectly adequate for the majority of C++ users, but people writing complex libraries, particularly if they are making use of templates, may well want to gain a deeper insight into exception handling.

Luckily, the creator of the C++ language (Bjarne Stroustrup) has provided some very clear guidelines on this subject. His most comprehensive comments on exception safety in the C++ standard library, together with guidelines for library writers in general, are in "Appendix E: Standard Library Exception Safety", from his book "The C++ Programming Language (Special Edition)".

Bjarne has kindly also put a copy of the appendix up on his website.