5.2 Exception Handling


Exception handling was once a largely documented but unsupported feature in many compilers. Those days are behind us. If you haven't read Stroustrup's chapter on exception handling, you definitely should. See [Stroustrup00]. Here we discuss practices that will help you to use exception handling efficiently and to avoid its overuse.

graphics/dia.gif BRIEF OVERVIEW

Exception handling can be summarized in a simple example:

 int main() {   try {     // Your application goes here   }   catch (...) {     std::cerr << "Exception caught" << std::endl;     return 1;   }   return 0; } 

By placing your application inside a try() block, the listed exceptions can be caught. In this case, all exceptions are caught. This is not uncommon inside main() because it acts as the last chance to catch an exception. If no exception handler is defined, std::terminate() is called, which then calls abort() . You can use throw to generate an exception, using a built-in class or one of your own, as shown:

 throw std::overflow_error ("Out of range"); 

This uses the standard library overflow_error class for the exception. This class takes a string, containing whatever information you want, during construction. We can modify our example slightly to catch specific errors:

 int main() {   try {     // Your application goes here   }   catch (std::exception& ex) {     std::cerr << "Standard Library exception caught: " << ex.what()               << std::endl;     return 1;   }   catch (...) {     std::cerr << "Exception caught" << std::endl;     return 1;   }   return 0; } 

The order in which you list catch() blocks is very significant, because the first matching block will execute. In this example, we are catching any object of type std::exception ” we also catch any exception derived from this class. If the throw statement above is executed, the following will be displayed to cerr :

 Standard Library exception caught: Out of range 

graphics/dia.gif EXCEPTIONS IN CONSTRUCTORS AND DESTRUCTORS

In general, you can generate exceptions or catch them whenever you want. However, the use of exceptions in constructors and destructors requires care to avoid heap or resource leakage, or unexpected program termination.

The function of a constructor is to create and initialize an object. Since a constructor does not return a value, you may have wondered what your application can do when an error is detected during construction. Because constructors are not intended to fail, throwing an exception is a reasonable strategy. This strategy allows you to place code at a higher level, where it can best be decided how to handle the error. What happens is that, even though the object may not be completely constructed , the destructor is called for all class members that were fully initialized before the exception is thrown. As a result, the object is brought back to the state it was in before the object was constructed.

To prevent heap or resource leakages when an exception is thrown inside a constructor, you should use the Resource Acquisition Is Initialization (RAII) technique we discussed on page 130. By wrapping a resource object inside another object, it is guaranteed that the resource's destructor will be called if the resource object was constructed before an exception is thrown. The resource's destructor will release the resource and return the system to the state it was in before the original constructor was ever called. If you do not use this technique, you can't guarantee that an exception thrown from within a constructor will not cause a heap or resource leak. For detailed information, see [Stroustrup00].

Generating exceptions within a destructor must be avoided. To see why, let's consider what happens when an exception is thrown. A catch handler is written to handle an exception and continue execution. Objects constructed inside the try() block are destroyed when the exception is caught. If any of these destructors were to generate an exception of their own, the system would be in a hopeless state because there is no way to know what the proper course of action should be. If this condition actually happens, the application calls terminate() . Unless you write a custom termination handler, abort() is called and the application shuts down. As a matter of practice, exceptions should never be called from within a destructor unless you catch them before they propagate outside the destructor.

graphics/dia.gif CATCHING EXCEPTIONS

Regardless of how much or how little you use exception handling, you need to take some steps to catch any errors before they cause your application to terminate. Even if your code does not use exception handling, the standard library does. At the very least your application needs to have a top-level catch handler, as we showed above. We recommend two additions to your top-level catch handler. The first is to catch specific types of errors before your catch-all handler does. Your application should make every attempt to restart itself, or gracefully fail, before you give up and terminate the application. The second addition is to include another catch handler as a backup to the first. Adding too much logic in the first catch handler can actually trigger another exception. The backup catch handler should attempt to write error information to an error log or console, exit, and then if possible restart the application.

To get in the habit of having a top-level catch handler, you should put your application code in a function other than main() . This will make it easier to add an exception handling scheme to suit your needs, as shown.

 #include "debugstream.h" int yourMain (int restart) {   // Your main function goes here } int catchMain() {   int retval   = 0;   bool running = true;   int restart  = 0;  // Restart count   while (running) {     try {       // Run the application       retval = yourMain (restart);     }     catch (std::exception& ex) {       cdebug << "catchMain: Standard Library exception caught: "             << ex.what() << ". Restarting ..." << std::endl;       restart++;       // Add code to decide if we should fail instead of restarting     }     catch (...) {       cdebug << "catchMain: Unknown exception caught" << std::endl;       retval = 1;       running = false;     }   }   cdebug << "catchMain: Stopping with exit code " << retval          << std::endl;   return retval; } int main() {   // Set up our debug stream   debugstream.sink (apDebugSinkConsole::sOnly);   apDebugSinkConsole::sOnly.showHeader (true);   return catchMain (); } 

In this example, yourMain() is the function that runs your application. It takes a parameter, restart , that lets the application know if this is the first time it is run, or whether the system is restarting after an error. Many applications are written with an event loop, so restarting the application after an error is certainly possible. Your application code can attempt to restart the application and preserve as much state information as possible. For instance, your application might display an error message that tells the user an error has occurred, and allows them to decide whether to continue using the application without restarting it.

The call to yourMain() is wrapped in a try block. We have shown two catch statements: one for standard library exceptions, and a catch-all for any others. You can add catch statements for other categories of errors, especially for exceptions that you define. In our example, we restart the application after any standard library exception is caught. Please keep in mind that global and static objects are not reinitialized after an exception is caught and yourMain() is called again. You must explicitly reinitialize any global and static objects to prevent subtle bugs from appearing in subsequent runs of yourMain() . We could have defined the variable restart to be a bool , but then the application would not know how many times the application was restarted. By using an int , the application can decide what should happen if the application is restarted too many times. For example, if we define yourMain() to be:

 int yourMain (int restart) {   throw std::overflow_error ("Out of range");   return 0; } 

the application will go into an infinite loop and send countless error messages to the console. This is clearly not what should happen, and you can rectify this situation inside yourMain() or catchMain() .

Before you think the design issues of a top-level catch handler are resolved, you must consider what happens if an exception is caught in catchMain() , which throws an exception. In our example, the catch handlers are short and simple. In reality they may be much more complicated, and we must assume that exceptions might be thrown within them. We recommend adding a second level of exception handling by splitting catchMain() into two pieces, as shown:

 int primaryMain() {   int retval   = 0;   bool running = true;   int restart  = 0;  // Restart count   while (running) {     try {       // Run the application       retval = yourMain (restart);     }     catch (std::exception& ex) {       cdebug << "primaryMain: Standard Library exception caught: "             << ex.what() << ". Restarting ..." << std::endl;       restart++;       // Add code to decide if we should fail instead of restarting     }     catch (...) {       cdebug << "primaryMain: Unknown exception caught" << std::endl;       retval = 1;       running = false;     }   }   cdebug << "primaryMain: Stopping with exit code " << retval          << std::endl;   return retval; } int catchMain() {   try {     // Run the application     return primaryMain ();   }   catch (...) {     cdebug << "catchMain: Unknown exception caught" << std::endl;     return 1;   }   return 0; } 

We have moved the functionality that was in catchMain() into a new function primaryMain() . The new catchMain() function calls primaryMain() but catches all errors. No recovery is attempted. In this example, we write a string to cdebug and exit. In production code, it is best to save the state of the application in such a way that it uses as few resources as possible. The only reason the catch handlers inside catchMain() are used is if some kind of catastrophic failure has occurred.

graphics/dia.gif CATCHING MEMORY ALLOCATION ERRORS

What happens if a call to operator new fails because there is insufficient memory? A user-defined error handler function will be called if one is defined. Otherwise , std::bad_alloc() is called. A handler function is set by calling std::set_new_handler() with a pointer to the function. If all you want to do is generate an error when there is insufficient heap, the solution is simple:

 void newHandler () {   cdebug << "memory allocation failure" << std::endl;   throw std::bad_alloc (); } int main () {   std::set_new_handler (newHandler);   ... } 

Once newHandler() is established as our error handler, it will be called when any heap allocation fails. The interesting thing about the error handler is that it will be called continuously until the memory allocation succeeds, or the function throws an error. If you were to write the function as:

 void newHandler () {} 

your application will be effectively dead if a heap allocation error occurs. Since no error is thrown by the handler, the allocation will be attempted again once newHandler() is done, which will simply fail again.

If you decide to use global handlers, such as std::set_new_handler() , you should only use them in top-level functions. These handlers should not be defined in resusable pieces of software because this prevents your application from using these features. If your global handlers only deal with one or more special cases in your application, remember to call the previous handler function to deal with any cases you do not process.

In our original handler there is a danger of triggering another heap allocation error when writing to the I/O stream (using our cdebug stream). Issues like these should always be considered when writing global handler functions. To solve this problem, we create a Singleton object, apHeapMgr , to catch heap errors and also attempt some limited recovery.

We want to try and recover from a heap allocation error, and the best way to do this is to reserve a block of heap memory when the application starts. The idea is that the handler releases this heap memory when an error occurs, allowing the allocation to succeed. This technique doesn't solve all heap- related problems, but if the size of the reserve buffer is large enough, at least the application can continue running. And, if you can't continue running, it is very important to notify users and give them a chance to save the state of the application before another error occurs. Heap exhaustion does not just happen when there is no more heap memory available. If heap becomes fragmented , it is very possible that an allocation of the desired size is not possible. The apHeapMgr Singleton object (because we have only one instance of it) is defined here.

 class apHeapMgr { public:   static apHeapMgr& gOnly ();   int  allocate (int desiredSize);   void release  () { cleanup();}   // Allocate or release the heap memory. This can be done   // manually or in response to a low memory condition.   // allocate() returns the number of bytes reserved   void automatic (bool state) { state_ = state;}   // When set to true (the default), a low memory condition   // will automatically release any reserved memory. private:   static void newHandler ();   // Our memory handler when new cannot allocate the desired memory   static apHeapMgr* sOnly_;   apHeapMgr ();   void releaseMemory (); // Release reserve memory   void cleanup ();       // Release our reserve and unhook our handler   new_handler oldHandler_; // Previous handler   bool        state_;      // true if we will automatically release memory   bool        nested_;     // Detects if newHandler() is nested   int         size_;       // Amount of memory (bytes) in reserve   char*       reserve_;    // Reserve memory we keep }; 

newHandler() is a static method that is the actual error handler. allocate() is used to create the reserve memory buffer. The desired size is passed as an argument. To be safe, the reserve buffer is obtained using a while loop, reducing the desired size by half until the memory allocation is successful. Once the reserve buffer is obtained, newHandler() is set as the error handler function. release() is the opposite of allocate() , and will release any memory buffer and deactivate the error handler. automatic() directly controls when the reserve buffer may be used. When set to true (the default), the reserve buffer is freed when a heap allocation error occurs. Setting the state to false disables the error handler, putting the user in charge of when the reserve buffer can be released.

The source code can be found on the CD-ROM, but the error handler function is repeated here.

 void apHeapMgr::newHandler () {   if (apHeapMgr::gOnly().nested_) {     // We have recursed into our handler which is a catastrophe     throw std::bad_alloc ();   }   apHeapMgr::gOnly().nested_ = true;   if (apHeapMgr::gOnly().state_) {     // Free our memory if we have not already done so     if (apHeapMgr::gOnly().reserve_) {       apHeapMgr::gOnly().releaseMemory ();       apHeapMgr::gOnly().nested_ = false;       return;     }   }   cdebug << "Throwing bad_alloc from newHandler" << std::endl;   apHeapMgr::gOnly().nested_ = false;   throw std::bad_alloc (); } 

This is a static function, so we must use the Singleton object, apHeapMgr::gOnly() , to reference it. A flag, nested_ , is used to see if newHandler() was called during the execution of newHandler() . This recursion means our error handler has itself generated an allocation error. At this point, it is unsafe to do anything other than throw an error and exit. If the reserve buffer is available and we are allowed to automatically release it ( state_ is true ), the buffer is freed and the handler ends. In all other cases std::bad_alloc() is thrown after an error message is generated.

graphics/dia.gif USING EXCEPTION SPECIFICATIONS

We have not used exception specifications in our production code. The specifications are instructions to the compiler regarding what exceptions a function can throw. For example:

 void function() throw (apException1); 

This specification says that function() can throw only apException1 or an exception derived from apException1 . apException1 can represent any exception, either user-defined or system-defined. Other possibilities are:

 void function();           // All exceptions can be thrown void function() throw ();  // No exceptions can be thrown 

You would think that we would be endorsing these specifications as a way to improve your code. Unfortunately, there are a couple of issues that prevent us from doing so.

The first issue is that specifications are not completely checked at compile time. If your code throws an exception that is not listed in your specification, a function called std::unexpected() is called. If you do not define your own function by way of std::set_unexpected() , the application will terminate. That is an extremely harsh thing to do, and it does not matter if you have a catch-all handler defined in your code. In addition, you are somewhat limited in what your std::unexpected() function can do. For example, you can certainly do this:

 void myUnexpected () {   std::cout << "myUnexpected" << std::endl; } void willthrow () throw () {   throw std::overflow_error ("Out of range"); } int main() {   std::set_unexpected (myUnexpected);   try {     willthrow ();   }   catch (...) {     std::cout << "catch-all" << std::endl;   }   return 0; } 

You might be surprised at what happens. In this example, a willthrow() function is called, which is defined not to throw any errors. However, it does throw an exception. willthrow() is wrapped in a try block to catch all errors. set_unexpected() is first called to install our handler, myUnexpected() , to run instead of std::unexpected() . This example will do one of the following things:

  • Fail to compile or generate warnings, because willthrow() violates the exception specification

  • Compile, execute, and display myUnexpected on the console, and then std::terminate()

  • Compile, execute, and display catch-all on the console.

The behavior is compiler-specific and should be documented in the release notes of whatever C++ compiler you are using. However, this discrepancy makes it unusable for multi-platform products. Obviously, the desired solution is to have the compiler detect and flag all invalid exception specifications. If this were the case, we would probably be using this feature. You might expect that the catch-all will run, but this is contrary to what exception specifications are all about. If the compiler can only determine at run-time that a compiler specification has been violated, it must shut down the application or call your handler function first.

Can exceptions be thrown from within our handler function? The answer is yes, an error can indeed be thrown. The problem is that this exception must be listed in every exception specification that may be traversed until the exception is caught. If this is not done, the application will call std::terminate() . For a large system, this amounts to adding an exception specification to every function, unless you understand the dynamics of your application perfectly . It is also important that you catch all exceptions within your destructor; otherwise, std::terminate() will be called as well in this case.

If you do plan on using exception specifications, read the documentation carefully . On Microsoft Windows systems, for example, each thread maintains its own std::unexpected() function. You must call std::set_unexpected() after any thread is created. Our rules for handling exceptions are shown in Figure 5.1.

Figure 5.1. Exception Rules

graphics/05fig01.gif

5.2.1 Designing Your Own Exception Framework

So far we have reviewed how exceptions work and some of the potential pitfalls you should watch for in your code. An application should define its own set of exceptions that define application-specific error conditions. Reusing standard library exceptions is one possibility, if they map nicely to existing exceptions. For example, std::overflow_error is an excellent choice for a generic overflow error. It is non-specific, but it also takes a string that can detail the source of the error. The disadvantage of using both application-specific (your own) and standard library exceptions is that your catch handlers become more complicated, because you need to deal with both of them.

Designing your own exception framework is not difficult because little information needs to be carried with an exception. A string is usually sufficient to detail the specific error. To address this issue of having two different exception frameworks (your own and standard library exceptions), we derive our apException base class from std::exception . The base class defines a virtual function, what() , that returns a string describing the error, as shown.

 class apException : public std::exception { public:   apException (std::string name)     : std::exception (), name_ (name) {}   virtual ~apException () {}   virtual const char* what () const   { return name_.c_str();} protected:   std::string name_; }; 

We don't recommend that you derive exceptions directly from std::exception because there is no way to separate standard library exceptions from your own. Adding your own base class, like apException , allows you to write code to only catch your own exceptions, such as:

 try {   ... } catch (apException& ex) {   // Our exception } catch (...) {   // Some other exception } 

But you can also write code, to catch your own exceptions, as well as standard library ones, like this:

 try {   ... } catch (std::exception& ex) {   // Our exception or any standard library exception } catch (...) {   // Some other exception } 
graphics/star.gif

Derive an object from apException for each type of exception you expect to have in the application.


For example, suppose your imaging application frequently verifies that the coordinates passed to a function are correct. Using std::out_of_range error is not appropriate because this error is not specific enough. We can derive an object, apBoundsException , to handle this error condition, as shown here.

 class apBoundsException : public apException { public:   apBoundsException (std::string name="")     : apException ("apBoundsException: " + name) {} }; 

We have written apBoundsException so that any additional information is optional. The name of the exception is pretty specific so that the string information is not necessary. In this example:

 try {     throw apBoundsException ("Hello");   } catch (apBoundsException& ex) {     std::cout << "caught " << ex.what() << std::endl;   } 

we display the message, caught apBoundsException: Hello on the console. And because of the virtual what() function, this line of output is identical if the catch block is any of the following:

 catch (apBoundsException& ex) { ... }   catch (apException& ex) { ... }   catch (std::exception& ex) { ... } 

5.2.2 Avoiding Exception Abuse

Exceptions are often overused . Let's look at an example:

 char getPixel (int x, int y) {   if (x < 0  x >= width()        y < 0  y >= height())     throw apBoundsException ("getPixel");   ... } 

There is nothing wrong with this example at first inspection. If the caller passes invalid arguments to getPixel() , it throws apBoundsException() . But let's look at how this function might be used:

 int sum () {   int total = 0;   for (int y=0; y<height(); y++) {     for (int x=0; x<width(); x++) {       total += getPixel (x, y);       ...     }   }   return total; } 

If you wrote sum() , would you include a try/catch block? And if you did, you might write one of the following:

 int sum1 () {   int total = 0;   try {     for (int y=0; y<height(); y++) {       for (int x=0; x<width(); x++) {         total += getPixel (x, y);       }     }   }   catch (apBoundsException& ex) {     cdebug << ex.what() << std::endl;   }   return total; } int sum2 () {   int total = 0;   for (int y=0; y<height(); y++) {     for (int x=0; x<width(); x++) {       try {         total += getPixel (x, y);       }       catch (apBoundsException& ex) {         cdebug << ex.what() << " at " << x << "," << y << std::endl;       }     }   }   return total; } 

sum1() wraps the entire function in a single try block. If an exception occurs, it prints a message and returns the current total. sum2() wraps each call to getPixel() , which allows the error message to be more clear. An exception will print a message and the loops will continue to run.

sum2() is the most effective at trapping errors but is also the slowest, since the try block is set up width()*height() times. As we have written these functions, an exception will never be thrown because the loops never deliver invalid coordinates. In this case, it makes no sense to incur the expense of a catch handler on every iteration.

graphics/star.gif

Consider a function's purpose before adding exception support. You may find a more optimal place to add the exception.


Low-level functions are not good candidates because they are called frequently. In our example, a better solution is to call the exception where it is generated, as shown here.

 class apImage {   ...   char getPixel (int x, int y);   //Description Fetch the contents of a single pixel.   //            No exceptions are thrown. This is the responsibility   //            of the caller.   //Parameters  x,y are the 0-based coordinates of the pixel   //Returns     Pixel value at (x,y). Returns 0 if the coordinates   //            are invalid.   int sum ()   //Description Compute the sum of all pixels in the image   //Returns     Sum of all pixels. }; 

You must document your exception strategy. In our example, it is very clear from the comments what getPixel() does. It states that exceptions are not thrown, and that is returned if invalid coordinates are passed. sum() does not say anything about exceptions because none are thrown; and the reason none are thrown is that sum() knows as much information about the size of the image as getPixel() does.

Sometimes it makes sense to consider whether it is appropriate to even use exceptions.

graphics/star.gif

Consider whether or not exceptions are the appropriate mechanism before routinely adding them.


For example, if we eliminate exception support from our previous example, it makes this code much simpler, as shown here.

 char apImage::getPixel (int x, int y) {   if (x < 0  x >= width()        y < 0  y >= height())     return 0;   ... } int apImage::sum () {   int total = 0;   for (int y=0; y<height(); y++) {     for (int x=0; x<width(); x++) {       total += getPixel (x, y);     }   }   return total; } 

In this case, it really makes sense to eliminate exception support in getPixel() since the caller can easily determine if the coordinates are valid. In apImage::getPixel() above, the function returns if the coordinates are invalid. This is a silent check, meaning that no error is generated if this is ever true. If getPixel() is used properly, this condition will never be relevant.

C-style functions typically use the return value to indicate an error condition, as shown here.

 FILE* fp = fopen ("file", "r"); if (fp == NULL) {   ... } 

Is there anything wrong with doing this? Absolutely not. After all, if you use std:: ifstream to read from a file, it doesn't throw an exception if the file does not exist. Exceptions are intended for exceptional cases; that is, cases that are not expected in the normal course of execution. Trying to open a nonexistent file might be considered a fatal error in some applications, but in general, an application should always test to see if the operation succeeds. Before you call throw from a function, you should consider whether the error is really an exception. We prefer to do things like this:

 bool compile (...); //Description Compile our data into a fast, binary format. //Returns     true if the compilation was ok, false on failure 

or perhaps:

 std::string compile (...); //Description Compile our data into a fast, binary format. //Returns     null string on success, else error description 

graphics/dia.gif WHEN TO USE EXCEPTIONS

It seems like we are finding lots of reasons not to use exceptions. So, when should they be used? If it is very unusual for a function to fail, you might consider using an exception. One of the disadvantages of functions that return status is that you have to check the status. If you have a large nesting of functions, this means that checks must be performed at every level.

Another good time to use exceptions is when a number of processing steps are treated as one unit. For example, consider a machine vision system that receives a signal of some sort , takes a picture (acquiring an image), and then processes and subsequently analyzes the image. If you take a high-level look at these steps, each step must be completed before the next step can begin, as follows :

 bool inspect () {   try {     acquire ();     process ();     analyze ();   }   catch (apException& ex) {     ... //Report error     return false;   }   ...   return true; } 

Let's look at the process() step and what it might do:

 void process () {   int i;   bool ok;   for (i=0; i<nFilterSteps; i++) {     ok = filterImage (i);     if (!ok) throw apException ();   }   for (i=0; i<nMorphSteps; i++) {     ok = morphImage (i);     if (!ok) throw apException ();   } } 

In our example, process() performs a number of image filtering steps, followed by a number of image morphology steps. An error at any step during this processing is considered a fatal error for the application. The functions, filterImage() and morphImage() , however, are low-level image processing routines that return their status instead of throwing exceptions, because this is handled by the application.

If you look at the inspect() function, we have the opposite behavior. We catch any errors during processing and return the overall status as a bool . This makes sense because whoever calls inspect() is interested in the result, and it is up to inspect() to catch and handle any relevant exceptions. We did not try to catch all errors here because we are expecting only errors derived from apException (or application-specific errors) to be thrown. If a different exception is thrown while inspect() is running, it will be caught by a higher-level catch handler; probably the one installed when the application started. For instance, what would happen if a memory error is thrown ( std::bad_alloc )? inspect() would not know how to handle this condition, but some other piece of code will, and that is why we only catch those errors we know how to handle.

Another appropriate time to use exceptions is when an unrecoverable error occurs deep inside a program, such that it cannot be handled at that level. For example, if a low-level function runs out of memory trying to allocate a temporary buffer, there is little that can be done. If you do not catch this exception yourself, the application's top-level catch handler will catch the error. However, your routine has the opportunity to log this error, perhaps to the console or to a log file, and then generate a more specific error that describes what happened . When you implement your top-level catch handler, reserve a memory buffer that can be released when the user needs to save the state and restart the application. The information describing the error can help the development team decide if this is indeed a memory error, or another type of bug in the system.

5.2.3 Using Assertions

Assertions are conditional statements that test for abnormal conditions and terminate the program, should they be false. Assertions are typically used only during debugging, when the symbol NDEBUG is not defined (see assert.h for details). During production builds, NDEBUG is defined and these statements effectively do nothing.

If your development environment does not have the notion of debugging versus production builds, you do not want to use assertions. An assertion will not only stop the system, it will also display details about where the error occurred, as shown:

 void process (apImage* p) {   assert (p);   ... } 

If a null pointer is passed to process() , the assertion will be false and the application will terminate. Presumably this statement is here as a check, because process() must be called with a valid pointer. During testing when the assertion is active, this statement is an effective means of finding aberrant code. But if null is passed to the production build, the behavior is unclear.

In debug builds, use assertions to enforce whatever restrictions you place on a function. If the comments for a function describe certain requirements that must be true, assertions are an excellent way to guarantee this. We can modify the getPixel() function we showed earlier, as follows:

 char apImage::getPixel (int x, int y) {   assert (x >= 0 && x < width());   assert (y >= 0 && y < height());   if (x < 0  x >= width()        y < 0  y >= height())     return 0;   ... } 

At first glance this function may appear flawed. After all, if assert() is used to make sure the coordinates are valid, why do we test it again? Just remember what happens if this is a production build. The assertions are missing in this case, and there is nothing to enforce the coordinate values. Whether you need both of these checks depends a lot on the application and how well it is tested . It is extremely difficult to completely test an application.

Keep in mind that the debugging version of the software differs in more ways from the production version than just the addition of assertion statements. Often the debugging software has different timing characteristics or other behavioral differences that can mask bugs that may actually exist in the production version.

We offer you the following guidelines for using assertions in Figure 5.2

Figure 5.2. Assertion Rules

graphics/05fig02.gif



Applied C++
Applied C++: Practical Techniques for Building Better Software
ISBN: 0321108949
EAN: 2147483647
Year: 2003
Pages: 59

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net