C++ Exceptions

From EDM2
Jump to: navigation, search

Written by Gordon Zeglinski

Introduction

So what happened to the rest of the screen saver DTS series? Seems life is full of mini-disasters up here. Last month I injured my shoulder which made typing impossible. This month I had to reinstall OS/2 and reformat the hard disk. I have not yet reinstalled the SOM toolkit or Metaware's High C/C++. Until I have time to do so, the DTS articles will have to wait. Instead, we will talk this month about C++ exceptions.

C++ exceptions are a powerful method of handling special cases. Traditionally, one might do something like:

  switch( foo(MyArguement,&var) ){
      case 0:     //alls well
          break;
      case 1:
          foo(0,&var);    //myarguement is invalid..
          break;
      case 2;             //out of memory or other serious error
          exit(2);
          break;
      default:
          fprintf(stderr,"Unknown Error returned from foo\r\n");
          break;
  }

Figure 1: Traditional method of error code handling.

Depending on the return value from foo(), an error condition may or may not exist. Exceptions follow a similar concept but are far more powerful.

What is Exactly is a C++ Exception?

An exception is an object designed by the programmer specifically to relay error information. They can be organized in hierarchies, as in the following example:

  class ErrorBase{
  public:
      ErrorBase(char *F,int L);
      ErrorBase(const ErrorBase &error);
      virtual ~ErrorBase();

      const char*     GetFile() const {return File;}
      int             GetLine() const {return Line;}

  protected:
      char    *File;
      int     Line;
  };

  class ErrorBadArg:public ErrorBase{
  public:
      ErrorBadArg(char *F,int L);
      ErrorBadArg(const ErrorBadArg &error);
      ~ErrorBadArg();

  };

  class ErrorNoMem:public ErrorBase{
  public:
      ErrorNoMem(char *F,int L);
      ErrorNoMem(const ErrorNoMem &error);
      ~ErrorNoMem();

  };

Figure 2: Sample exception hierarchy.

An advantage of using heirarchies of error objects is that any error object with a parent can be caught by blocks expecting the parent object. We'll look at catching exceptions by the exception object's parent class later.

Try, Catch and Throw

"Try" blocks are used to "catch" exceptions that are thrown in functions called within the block. We have already seen that an exception is nothing more than an object designed to convey error information. Unlike the simple switch case, however, try blocks can be nested within and across function calls. Also, exceptions don't rely on the function returning a value. The following sample code illustrates a nested exception.

  void foo1(){
      try{
          foo2();
      }
      catch(ErrorBase error){

          cerr<<"An exception was thrown from file "<<error.GetFile()<<" Line
  "<<error.GetLine()<<endl;

      }
      catch(...){ //some other error
          cerr<<"Some other exception has been thrown"<<endl;
      }

  }

  void foo2(){
      try{
          foo3(10);
      }
      catch(ErrorBadArg){
          try{
              foo3(0);
          }
          catch(ErrorBadArg){
              cerr<<"I give up. 0 is not a valid arg to foo3"<<endl;
          }
          catch(ErrorNoMem){
              cerr<<"We're Out of memory"<<endl;

              throw;            // I don't want to handle this let whoever
                                // called me get a kick at it

          }
      }
      catch(ErrorNoMem){
          cerr<<"We're Out of memory"<<endl;
          throw;
      }
  }

  void foo3(int arg){
      if(arg==rand()%10)
          throw ErrorBadArg(__FILE__,__LINE__);

      if(arg==rand()%40)
          throw ErrorNoMem(__FILE__,__LINE__);
  }

Figure 3: Nested exception example.

Function foo1() calls function foo2(). Any exceptions rethrown by foo2() or that are not handled by foo2() can be caught by foo1(). In fact, the statement "catch (...)" in foo1() catches all exceptions that aren't explicitly handled. In this case, any exception other than those in the ErrorBase() family are caught by the catch (...) block. Function foo3() illustrates how to throw an exception.

Formal Exception Declaration

In order to make sure that a function only throws certain exceptions, C++ allows a list of exceptions to be given with the function declaration. Rewriting foo3() we have:

  void foo3(int arg) throw(ErrorBadArg,ErrorNoMem){
      if(arg==rand()%10)
          throw ErrorBadArg(__FILE__,__LINE__);

      if(arg==rand()%40)
          throw ErrorNoMem(__FILE__,__LINE__);
  }

Figure 4: Declaring a method to throw specific exception classes.

Now foo3() can only throw the exceptions ErrorBadArg and ErrorNoMem. An attempt to throw any other exception will result in a runtime error. This makes it easier for the programmer to use third party libraries. The program will not encounter any surprise exceptions because the set of exceptions which a routine may throw is guaranteed.

Note: foo3() must catch all exceptions in any routine it calls other than the two it declares as those to be thrown by it.

Runtime Exception Errors

There's two runtime errors that can occur in the exception handling code. First, the exception might not be caught by any try/catch block. Second, an exception that wasn't in the declaration list for a function might not be caught by the function or a programmer may try to throw an unlisted exception. In the first case, the "terminate" function is called, in the second, the "unexpected" function is called. The functions set_terminate() and set_unexpected() can be used to replace the default function with application specific ones. The following sample shows how to use set_terminate().

  typedef void (*pVdFn)();

  void My_Terminate(){
      cerr<<"My Terminate Called"<<endl;
      exit(1);
  }

  int main(){

      pVdFn oldFn=set_terminate(My_Terminate);    //replace the previous
                                                  //terminate fnc

      foo3(11);

      set_terminate(oldFn);                       //set the terminate fnc
                                                  //back to the original

      /* do other stuff here */

  return 0;
  }

Figure 5: Using set_terminate().

In the above sample, if an exception occurs in foo3(), My_Terminate() should be called because we didn't set up a catch/try block.

Note: set_unexpected() is used in the exact same way as set_terminate().

Gotchas

There are two areas to watch out for. The first one is resource leaks. The second one is an overreliance on exceptions to alter the programs flow. Let's look at the following sample function.

  void foobar(char *arg){
      char    *X=new char[300];

      DosRequestMutexSem(hMux,-1);
      if(arg==NULL)
          throw ErrorBadArg;


      DosReleaseMutexSem(hMux);
      delete [] X;
  }

Figure 6: Resource leaks with thrown exceptions.

There are 2 potential resource leaks in this function. Because the throw alters the programs flow, when the throw statement is executed the mutex semaphore will not be released and the allocated memory will not be returned to the heap. There are several different ways of fixing this problem. The easiest is to move the allocation and semaphore request to after then throw statement.

  void foobar(char *arg){
      if(arg==NULL)
          throw ErrorBadArg;

      char    *X=new char[300];

      DosRequestMutexSem(hMux,-1);


      DosReleaseMutexSem(hMux);
      delete [] X;
  }

Figure 7: Removing the leak by relocating the resource allocation.

Another approach would be to release the semaphore and free the memory before throwing the exception.

  void foobar(char *arg){
      char    *X=new char[300];

      DosRequestMutexSem(hMux,-1);

      if(arg==NULL){
          DosReleaseMutexSem(hMux);
          delete [] X;

          throw ErrorBadArg;
      }

      DosReleaseMutexSem(hMux);
      delete [] X;
  }

Figure 8: Removing the leak by freeing the resource before throwing an exception.

Finally, the most thorough way is to use objects to handle the memory and semaphore.

  template<class T> class MemBlock{
      typedef T *pT;
  public:
      MemBlock(size_t len){Block=new T[len];}
      ~MemBlock(){if(Block!=NULL) delete [] MemBlock;}

      operator pT(){return Block;}

  protected:
      pT  Block;
  };

  class MutexRequest{
      typedef unsigned long ULONG;
      typedef unsigned long HMTX;
  public:

      MutexRequest(HMTX hand,ULONG
  time){hmtx=hand;DosRequestMutexSem(hmtx,time);}

      ~MutexRequest(){DosReleaseMutexSem(hmtx);}

  protected:
      HMTX    hmtx;
  };

  void foobar(char *arg){
      MemBlock<char>  X(300);
      MutexRequest    sem(hMux,-1);

      if(arg==NULL)
          throw ErrorBadArg;

      /* do other stuff here */
  }

Figure 9: Removing the leak by performing resource management in the constructor and destructor.

This last approach has a hidden advantage. Suppose we called some other functions in foobar() and one of these functions threw an exception. What would happen in either of our first 2 versions of foobar()? The answer is that we would have the same resource leak that we fixed in the last 2 versions. Unless we add a try/catch block in foobar() and then place calls to DosReleaseMutexSem() and delete inside the catch block, we will leak resources. Encapsulating the resources and using constructors/destructors to handle their allocation and deallocation provides a safer alternative than manually maintaining the deallocation of resources in several possible exit points.

The second pitfall is using exceptions too liberally. Suppose we have a function that has a fixed set of valid results. Let's say that one result happen most of the time, but it is possible in some cases for the others to occur. Should we use exceptions? The answer is no. Exceptions should only be used to indicate error conditions, not to indicate which action a function performed. As we seen from the C++ compiler benchmarks several issues ago, throwing exceptions is expensive. Not only is throwing the exception expensive, but setting up a try block alone can have noticeable overhead. To keep performance acceptable, exceptions should only be used to indicate an error condition.

Wrapping Things Up

This concludes our look at C++ exception handling. We covered how to define exceptions, catching, and throwing exceptions. Also, we seen how to handle unexpected results by using the set_terminate() and set_unexpected() functions. Exceptions provide a formal method for handling and declaring error conditions. They have significant run time cost and should only be used where necessary.