An Exceptional Model by (10 February 2003) | Return to The Archives

Audience

This article is intended for software designers or software developers who are in the design phase. Basic knowledge of a language which supports exception handling, such as C++ or Java, is recommended. I don't think that's wrong, because I personally think that any designer should have at least a basic knowledge of programming.

1. Preface

This is not another document about what exception handling is, but more why it exist. When you read this document you will find out how exception handling can help you to design very robust and clean source-code. I like exception handling a lot, and that feeling grows almost daily. The reason is that, when using exception handling, you are inside a model which *tells* you when something is wrong, instead of you *asking* the model. And that's a very big difference. To me it comes near to polling (if-stamements) versus events (exceptions). This document will explain how this difference can be exploited by you as a designer / developer.
Within this document, I will use the words "sequential" and "exceptional". When I say the first, I mean the programming style in which you don't use exception handling to trap errors, which means that your program will execute exactly in the way as you wrote it. It will always "come back" from a funtion or method call. When I say "exceptional", I refer to the programming style where certain parts of your code, often the core parts, may throw so called "exceptions", and thereby interrupt the program flow.
To make this (important) difference more clear, I created two figures. They both show an obvious program flow, where the function or method called "Init" is the top-layer of the "unsafe" code. Besides, they show the situation in which the loading of a certain sound sample failes ("LoadSound"). Figure one shows the sequential model, in which the error "bubbles" up to "Main". Figure two however, shows the exceptional model, in which an exception is thrown.
Figure 1: the "sequential" model.
Figure 2: the "exceptional" model.
The sample code is written in C++, but it won't compile because variables are often not declared or statements are incomplete. It just shows what it should clarify.
Finally, please note that exception handling is not strictly related to object oriented programming. You can perfectly use exception handling in a C++ function based library or program. You will notice this in the following samples.

2. The Advantages

In my opinion, the advantages which come with exception handling can be placed in three groups: "Abstraction", "Valid Objects" and "Clean code". 2.1. Abstraction Most programs, whether they are written function based or object oriented, will have some kind of hierarchy in tasks like the figures shown in the preface. There will be a set of functions or objects, which are dedicated to resource loading. On top of that will be functions or objects which administrate these resources. And on top of that will be your actual application functions or objects, the ones that put all pieces together. In each "layer", something might go wrong. Therefore, each layer should report to it's calling layer, whether it succeeded performing his task. This is often done with 'if' statements. There is nothing wrong with that, but when you have quite some layers, you are performing a lot of checks. (As you can see in figure 1). For a code sample, take a look at the following layered program code.
Layer 1:

    // Returns a pointer to your Surface type, or NULL when something failed
    SurfaceTypePtr* LoadImg (string& sFilename) {
      // Call you favorite multimedia API
      SurfaceTypePtr pSurf = LoadImage (sFilename) ;
      if (!pSurf) {
        // So the caller can see that something went wrong
        return NULL ;
      }
      // Set certain attributes, such as colorkey or alphachannel
      ...
      return (pSurf) ;
    }

Layer 2:

    // Fills an administration and returns the result, with the help of the bottom layer
    bool LoadArt (vector& vecImg) {
      // Declare helper
      SurfaceTypePtr pSurfTmp = NULL ;
      // Load an image
      pSurfTmp = LoadImg ("Ship.bmp") ;
      if (!pSurfTmp) {
        // Atomicy
        vecImg.clear () ;
        return (false) ;
      }
      // Ok, store it
      vecImg.push_back (*pSurfTmp) ;
      // Load another one
      pSurfTmp = LoadImg ("Enemy.bmp") ;
      if (!pSurfTmp) {
        // Atomicy
        vecImg.clear () ;
        return (false) ;
      }
      // Ok, store it
      vecImg.push_back (*pSurfTmp) ;
      // And so on
      ...
      return (true) ;
    }

Layer 3:

    // Initializes and runs game
    int main (int, char**) {
      // Init video, sound and such
      ...
      if (!LoadArt (vecImg)) {
        return 1 ;
      }
      // Game loop
      while (bRun) {
        ...
      }
      // Cleanup
      ...
    }

Now, the idea of exception handling is that you succeed-test the core layers, but not the layers on top. When you look at the code in the bottom layer 1 (the one which loads one image), you see that we test whether it succeeded. Failure will probably happen when the file can not be found. However, in the function on top of it (the one which loads all art), we check whether the "LoadImg" function succeeded. This is kind of strange, because we are in fact testing the same thing. (Was the file found?). To go even further, in our main function we test if the art could be loaded, which is tested again. So, actually, in our top layer we are implicitly testing errors from two layers down(!).
Let's rewrite the three layers for an exceptional model, where we only test for an error at the lowest level.
Layer 1:

    // Returns a pointer to your Surface type, or throws an exception when something failed
    SurfaceTypePtr* LoadImg (string& sFilename) {
      // Call you favorite multimedia API
      SurfaceTypePtr pSurf = LoadImage (sFilename) ;
      // Here we do our test
      if (!pSurf) {
        // *Tell* the program something went wrong
        throw Exception ("LoadImg: could not load %s.", sFilename) ;
      }
      // Set certain attributes, such as colorkey or alphachannel
      ...
      return (pSurf) ;
    }

Layer 2: Since we "agreed" that we don't test here, we just don't.

    // Fills an administration and returns the result
    // First, we remove the return value, because we let escape an exception (or not of course).
    void LoadArt (vector vecImg) {
      // (We also don't need the helper anymore)
      // You see that this can be one call. We don't have to worry about a NULL argument,
      // because the call to "push_back" will only be made in case of succes in the
      // function "LoadImg". This type of programming saves a lot of intermidiate tests(!).

      // Load and store an image
      vecImg.push_back (*(LoadImg ("Ship.bmp")) ;
      // Load and store another image
      vecImg.push_back (*(LoadImg ("Enemy.bmp")) ;
    }

Layer 3: Finally, we have our top layer, which let our program be called a game. Here we set up a try-catch block.

    // Initializes and runs game
    int main (int, char**) {
      try {
        // Init video, sound and such
        ...
        LoadArt (vecImg) ;
        // Game loop
        while (bRun) {
          ...
        }
        // Cleanup
        ...
      }
      catch (Exception e) {
        cout << e ;
      }
    }

You really should not confuse a try-catch block with an if-block, because it is not. You should see it as a piece of container code, in which the part between 'try' and 'catch' executes. That container will only fulfill it's purpose when an exception comes in; like a safety-net in a circus, which only does it's work when an acrobat actually falls down.
2.2. Valid objects
(This argument is OOP-specific.)
When you define a class which depends on a resource, such as heap memory or a file, there are quite some problems you run up against when instantiating. First of all, a constructor does not have a return value. So, in a sequential model, you cannot immediately tell the user that memory allocation failed, or that a file could not be found or loaded. This implies that you should not call unsafe code from a constructor. However, there is a workaround, even in the sequential model. You could use a variable which works as a flag, and add a method with a name like 'isInitialized ()'. Then a constructor would look like:

    CObj::CObj (string& sFilename) {
      if (!LoadData (sFilename)) {
        bInitialized = false;
      }
      else {
       bInitialized = true ;
      }
    }

And a use of the object would look like this:

    CObj o ("data.txt") ;
    if (!o.isInitialized) {
      error
    }

When you put this inside a base class, from which you derive all your unsafe classes, you have a generic way to check for errors.
But... telling us that an object could not successfully load a file or allocate memory looks nice, but it doesn't help us that much. Why not? Because you can get stuck with useless objects(!). What do you gain with an Animation object which does not have anything to play, for example? You could protect your classes by putting an 'if (bInitialized)' inside each method, but in fact you don't want to have your program still running in this errornous state.
So, what we need is a way to only let an object pass construction when we can be sure that it will be useful. Construction fails when an exception "escapes". And within that knowledge lies our solution. When we make constructors throw exceptions, we ensure that, when it comes back, we have a valid object. Let's modify the code from above:

    CObj::CObj (string& sFilename) {
      if (!LoadData (sFilename)) {
        throw Exception ("The data from file %s could not be loaded.", sFileName) ;
      }
    }

And a use of the object would look like this:

    CObj o ("data.txt")
    // If the code does not reach here, an exception has passed us.

Note that it also makes your destructors more easy, because you don't have to worry about NULL pointers for example.
2.3. Clean code
Often, you have to implement a function differenly from how you have it in mind. For example, a function or method which returns a vector might be on your mind (or, even better, in your design document) like this:

    vector loadNames (string& sFile) ;

Unfortunately, in a sequential model, this has no room for a return value. These are the options:
1. vector loadNames (string& sFile, bool& bOk);
2. bool loadNames (string& sFile, vector& vecNames);
However, in the exceptional model you can just use the first one.
When you look at the differences, you will notice something important: using an exceptional model, you can write your higher level code, like you are in a perfect world where nothing can go wrong(!).

3. Troubles In Paradise

Although exception handling has some very big advantages, there are some issues you have to keep in mind. Some are listed and motivated below. 3.1. Inconsistency You must remember that when an exception occurs, you should be careful that you don't get stuck with inconsistent, or even erroneous code like a memory leak, or a broken chain, which might occur when an exception occurs during a pointer re-arrangement. So it is very important that you write your lower level code as beiing "atomic", which means that a piece of code will only change your program state when it succeeds completely. If anything fails, it does a "roll-back", before throwing the exception. Please note that this is the overall behaviour of the C++ standard library. Although not every container throws exceptions, most guarantee that they leave your contained data consistent when a call fails.
3.2. Default construction
When initialization is done inside a parameterized constructor, you must declare a private default constructor (no parameters) inside your class, otherwise you could declare an object with the default constructor, and still be left with a useless object(!).
3.3. Harder to test
Because an exceptional program does not have to follow a pre-ordered path, as we read in the preface, there are more branches which your program might walk into. So it might require more time to test your application.
3.4. Undeclarable objects
A class with a parameterized constructor can not be declared as a member of another class. For example, you may not write:

    ...
    private:
      CAnimation animJump ("heroJump.anm") ;
    ...

However, this drawback isn't hard to overcome. For parameterized constructors you might follow the Java paradigma, where you allocate your objects on the heap. So, a hero class might look like:

    ...
    private:
      CAnimation *pAnimJump ;
    ...

And in the constructor:

    CHero::CHero () {
      ...
      pAnimJump = new CAnimation ("heroJump.anm") ;
      pAnimShoot = new CAnimation ("heroShoot.anm") ;
      ...
    }

Please note that you may declare parameterized objects on the stack, like local variables.

4. Links

I decided to put some miscellaneous links about exception handling here. Their category is also mentioned. Of course, there is way more information around on the web but these might be some guidelines. Syntax and use:

> - http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/_core_Exception_Handling_Topics_.28.C.2b2b29.asp
> - http://java.sun.com/docs/books/tutorial/essential/exceptions/index.html

Dangers:

> - http://www.awprofessional.com/meyerscddemo/DEMO/MAGAZINE/CA_FRAME.HTM

Other:

> - The book Exceptional C++ Design, by Herb Sutter, has a great chapter about how to write Exception Safe code, which greatly helps in avoiding problems as mentioned in the "dangerous" article above.

5. Conclusion

Although exception handling has some traps, I think that it is well worth it to use it, and switch to it when you are not already. I hope I made clear that using exception handling is more a design / code-flow issue than it is a technical one. However, in large projects it will also save you lots of source-lines.
If you would like to discuss elements of this article with me, feel free to send an email to this address.

Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
Please read our Terms, Conditions, and Privacy information.