[17] Exceptions and error handling
(Część C++ FAQ Lite, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)


FAQ - sekcja [17]:


[17.1] What are some ways try / catch / throw can improve software quality?

By eliminating one of the reasons for if statements.

The commonly used alternative to try / catch / throw is to return a return code (sometimes called an error code) that the caller explicitly tests via some conditional statement such as if. For example, printf(), scanf() and malloc() work this way: the caller is supposed to test the return value to see if the function succeeded.

Although the return code technique is sometimes the most appropriate error handling technique, there are some nasty side effects to adding unnecessary if statements:

So compared to error reporting via return-codes and if, using try / catch / throw is likely to result in code that has fewer bugs, is less expensive to develop, and has faster time-to-market. Of course if your organization doesn't have any experiential knowledge of try / catch / throw, you might want to use it on a toy project first just to make sure you know what you're doing — you should always get used to a weapon on the firing range before you bring it to the front lines of a shooting war.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[17.2] How can I handle a constructor that fails?

Throw an exception.

Constructors don't have a return type, so it's not possible to use return codes. The best way to signal constructor failure is therefore to throw an exception.

If you don't have or won't use exceptions, here's a work-around. If a constructor fails, the constructor can put the object into a "zombie" state. Do this by setting an internal status bit so the object acts sort of like it's dead even though it is technically still alive. Then add a query ("inspector") member function to check this "zombie" bit so users of your class can find out if their object is truly alive, or if it's a zombie (i.e., a "living dead" object). Also you'll probably want to have your other member functions check this zombie bit, and, if the object isn't really alive, do a no-op (or perhaps something more obnoxious such as abort()). This is really ugly, but it's the best you can do if you can't (or don't want to) use exceptions.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[17.3] How can I handle a destructor that fails?

Write a message to a log-file. Or call Aunt Tilda. But do not throw an exception!

Here's why (buckle your seat-belts):

The C++ rule is that you must never throw an exception from a destructor that is being called during the "stack unwinding" process of another exception. For example, if someone says throw Foo(), the stack will be unwound so all the stack frames between the throw Foo() and the } catch (Foo e) { will get popped. This is called stack unwinding.

During stack unwinding, all the local objects in all those stack frames are destructed. If one of those destructors throws an exception (say it throws a Bar object), the C++ runtime system is in a no-win situation: should it ignore the Bar and end up in the } catch (Foo e) { where it was originally headed? Should it ignore the Foo and look for a } catch (Bar e) { handler? There is no good answer -- either choice loses information.

So the C++ language guarantees that it will call terminate() at this point, and terminate() kills the process. Bang you're dead.

The easy way to prevent this is never throw an exception from a destructor. But if you really want to be clever, you can say never throw an exception from a destructor while processing another exception. But in this second case, you're in a difficult situation: the destructor itself needs code to handle both throwing an exception and doing "something else", and the caller has no guarantees as to what might happen when the destructor detects an error (it might throw an exception, it might do "something else"). So the whole solution is harder to write. So the easy thing to do is always do "something else". That is, never throw an exception from a destructor.

Of course the word never should be "in quotes" since there is always some situation somewhere where the rule won't hold. But certainly at least 99% of the time this is a good rule of thumb.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[17.4] How should I handle resources if my constructors may throw exceptions? UPDATED!

[Recently added the example and the admonition to use a typedef (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

Every data member inside your object should clean up its own mess.

If a constructor throws an exception, the object's destructor is not run. If your object has already done something that needs to be undone (such as allocating some memory, opening a file, or locking a semaphore), this "stuff that needs to be undone" must be remembered by a data member inside the object.

For example, rather than allocating memory into a raw Fred* data member, put the allocated memory into a "smart pointer" member object, and the destructor of this smart pointer will delete the Fred object when the smart pointer dies. The template std::auto_ptr is an example of such as "smart pointer." You can also write your own reference counting smart pointer. You can also use smart pointers to "point" to disk records or objects on other machines.

By the way, if you think your Fred class is going to be allocated into a smart pointer, be nice to your users and create a typedef within your Fred class:

 #include <memory>
 
 class Fred {
 public:
   typedef std::auto_ptr<Fred> Ptr;
   ...
 };

That typedef simplifies the syntax of all the code that uses your objects: your users can say Fred::Ptr instead of std::auto_ptr<Fred>:

 #include "Fred.h"
 
 void f(std::auto_ptr<Fred> p);  
// explicit but verbose
 void f(Fred::Ptr           p);  
// simpler
 
 void g()
 {
   std::auto_ptr<Fred> p1( new Fred() );  
// explicit but verbose
   Fred::Ptr           p2( new Fred() );  
// simpler
   ...
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[17.5] How do I change the string-length of an array of char to prevent memory leaks even if/when someone throws an exception? UPDATED!

[Recently rewrote to clarify/strengthen (in 9/02). Click here to go to the next FAQ in the "chain" of recent changes.]

If what you really want to do is work with strings, don't use an array of char in the first place, since arrays are evil. Instead use an object of some string-like class.

For example, suppose you want to get a copy of a string, fiddle with the copy, then append another string to the end of the fiddled copy. The array-of-char approach would look something like this:

 void userCode(const char* s1, const char* s2)
 {
   char* copy = new char[strlen(s1) + 1];    
// make a copy
   strcpy(copy, s1);                         
//   of s1...
 
   
// use a try block to prevent memory leaks if we get an exception
   
// note: we need the try block because we used a "dumb" char* above
   try {
 
     
...insert code here that fiddles with copy...
 
     char* copy2 = new char[strlen(copy) + strlen(s2) + 1];  
// append s2
     strcpy(copy2, copy);                                    
//   onto the
     strcpy(copy2 + strlen(copy), s2);                       
//   end of
     delete[] copy;                                          
//   copy...
     copy = copy2;
 
     
...insert code here that fiddles with copy again...
 
   } catch (...) {
     delete[] copy;   
// we got an exception; prevent a memory leak
     throw;           
// re-throw the current exception
   }
 
   delete[] copy;     
// we did not get an exception; prevent a memory leak
 }

Using char*s like this is tedious and error prone. Why not just use an object of some string class? Your compiler probably supplies a string-like class, and it's probably just as fast and certainly it's a lot simpler and safer than the char* code that you would have to write yourself. For example, if you're using the std::string class from the standardization committee, your code might look something like this:

 #include <string>           // Let the compiler see std::string
 
 void userCode(const std::string& s1, const std::string& s2)
 {
   std::string copy = s1;    
// make a copy of s1
   
...insert code here that fiddles with copy...
   copy += s2;               
// append s2 onto the end of copy
   
...insert code here that fiddles with copy again...
 }

The char* version requires you to write around three times more code than you would have to write with the std::string version. Most of the savings came from std::string's automatic memory management: in the std::string version, we didn't need to write any code...

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


E-Mail E-mail the author
C++ FAQ LiteSpis treściSkorowidzO autorze©Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1h Oct 3, 2003