Chapter 3. What to Do When We Encounter an Error at Runtime

There are two types of runtime errors: those that are the result of programmer error (that is, bugs) and those that would happen even if the code were absolutely correct. An example of the second type occurs when a user mistypes a username or password. Other examples occur when the program needs to open a file, but the file is missing or the program doesn’t have permission to open it, or the program tries to access the Internet but the connection doesn’t work. In short, even if the program is perfect, things such as wrong inputs and hardware issues can produce problems.

In this book we concentrate on catching run-time errors of the first type, a.k.a. bugs. A piece of code written for the specific purpose of catching bugs will be called a sanity check. When a sanity check fails, i.e., a bug is discovered, this should do two things:

  1. Provide as much information as possible about the error, i.e., where it has occurred and why, including all values of the relevant variables.

  2. Take an appropriate action.

What is an appropriate action? We’ll discuss this later in more detail, but the shortest answer is to terminate the program. First, let’s concentrate on the information about the bug, called the error message. To diagnose a bug we provide a macro defined in the scpp_assert.hpp file:

#define SCPP_ASSERT(condition, msg)        
  if(!(condition)) {                       
    std::ostringstream s;                  
    s << msg;                              
    SCPP_AssertErrorHandler(               
      __FILE__, __LINE__, s.str().c_str());
  }

SCPP_AssertErrorHandler is the function declared in the same file. (As it was mentioned earlier, the code of all C++ files cited in this book is available both in the Appendices and online at https://github.com/vladimir-kushnir/SafeCPlusPlus.)

First, let’s see how it works. Suppose you have the following code in the my_test.cpp file:

#include <iostream>
#include "scpp_assert.hpp"

using namespace std;

int main(int argc, char* argv[]) {
  cout << "Hello, SCPP_ASSERT" << endl;

  double stock_price = 100.0;   // Reasonable price
  SCPP_ASSERT(0. < stock_price && stock_price <= 1.e6,
    "Stock price " << stock_price << " is out of range");

  stock_price = -1.0; // Not a reasonable value
  SCPP_ASSERT(0. < stock_price && stock_price <= 1.e6,
    "Stock price " << stock_price << " is out of range");

  return 0;
}

Compiling and running the example will produce the following output:

Hello, SCPP_ASSERT Stock price -1 is out of range in file
      my_test.cpp #16

The macro automatically provides the filename and line number where the error occurred. What’s going on in here? The macro SCPP_ASSERT takes two parameters: a condition and an error message. If the condition is true, nothing happens, and the code execution continues. If the condition is false, the message gets streamed into an ostringstream object, and the function SCPP_AssertErrorHandler() is called. Why do we need to stream the message into the ostringstream object? Why can’t we just pass the message to the error handler function directly?

The reason is that this intermediate step allows us not just to use simple error messages like this:

SCPP_ASSERT(index < array.size(), "Index is out of bounds.");

but to compose a meaningful error message that contains much more information about an error:

SCPP_ASSERT(index < array.size(),
  "Index " << index << " is out of bounds " << array.size());

In this macro you can use any objects of any class that has a << operator. Suppose you have a class:

class MyClass {
 public:
  // Returns true if the object is in OK state.
  bool IsValid() const;

  // Allow this function access to the private data of this class
  friend std::ostream& operator <<(std::ostream& os, const MyClass& obj);
};

All you need to do is provide an operator << as follows:

inline std::ostream& operator <<(std::ostream& os, const MyClass& obj) {
  // Do something in here to show the state of the object in
  // a human-readable form.
  return os;
}

and you can use an object of the type MyClass to compose a message:

MyClass obj(some_inputs);
SCPP_ASSERT(obj.IsValid(), "Object " << obj << " is invalid.");

Thus, if you run your program and the sanity check detects an error, chances are that you won’t need to repeat the process in the debugger to figure out what exactly happened and why. But doing this sanity check might slow down your program, and the reason we’re using C++ is we want our code to run as fast as possible. And indeed, sanity checks do slow down the code, some of them significantly (as we’ll see later when dealing with the Index Out Of Bounds error in Chapter 4). To deal with this problem, some of the sanity checks are made temporary—for testing only. For this purpose, the scpp_assert.hpp file defines a second macro, SCPP_TEST_ASSERT:

#ifdef SCPP_TEST_ASSERT_ON
#define SCPP_TEST_ASSERT(condition,msg) SCPP_ASSERT(condition, msg)
#else
#define SCPP_TEST_ASSERT(condition,msg) // do nothing
#endif

The difference between this SCPP_TEST_ASSERT and the previous SCPP_ASSERT is that SCPP_ASSERT is a permanent sanity check, whereas SCPP_TEST_ASSERT can be switched on and off during compilation (more about this in Chapter 15). Now let’s return to the second question of what to do when a bug is discovered at runtime: what is the appropriate action in this case? Actually, there are only two choices:

  1. Terminate the program.

  2. Throw an exception.

The code of the error handler function provided in the scpp_assert.cpp file gives you both opportunities:

void SCPP_AssertErrorHandler(const char* file_name,
                             unsigned line_number,
                             const char* message) {
 // This is a good place to put your debug breakpoint:
 // You can also add writing of the same info into a log file
 // if appropriate.

#ifdef SCPP_THROW_EXCEPTION_ON_BUG
  throw scpp::ScppAssertFailedException(
    file_name, line_number, message);
#else
  cerr << message << " in file " << file_name
       << " #" << line_number << endl << flush;
  // Terminate application
  exit(1);
#endif
}

As you can see from the code of the error handler, it could do either of the two possible actions, depending on whether the symbol SCPP_THROW_EXCEPTION_ON_BUG is defined. In the most common case, when you want to test your code until you find the first bug, the simplest action by default is to terminate the program, fix the bug, and start over, which is achieved when the symbol SCPP_THROW_EXCEPTION_ON_BUG is not defined. In this case the error handler will print the message and terminate the application.

There are some situations when at least some of the sanity checks are left active in the code even in production mode. Suppose you have a program that does continuous sequential processing of a large number of requests, one after another, and while processing one of the requests it ran into a bug, i.e., the sanity check failed. It might so happen that the program could continue to process some of (and maybe even most of) the other requests. In some situations it might be important to continue to process these requests as much as possible—because it’ll keep clients happy, because there’s a serious amount of money involved, etc. In such cases, terminating the program on a failure of a sanity check is not an option. The way to proceed in these situations is to throw an exception containing a description of what happened from the error handler, catch it somewhere in the top level of the code, document it in some log file, maybe send some email or pager alerts, declare the current attempt to process the request a failure, and at the same time continue with all the others.

To illustrate this, an exception class that is declared in the same scpp_assert.hpp file:

namespace scpp {
class ScppAssertFailedException : public std::exception {
 public:
  ScppAssertFailedException(const char* file_name,
                            unsigned line_number,
                            const char* message);

  virtual const char* what() const throw () {
    return what_.c_str();
  }

  virtual ~ScppAssertFailedException() throw () {}

 private:
  std::string what_;
};
} // namespace scpp

If you are strict about exception types, you can pass to the error handler an enum containing information about error type, and throw different types of exceptions for different types of errors. But this book demonstrates a general approach to writing code with the explicit goal of self-diagnosing bugs, so we’ll stick with the simplest possible case of one exception class. In this case, the code example that would trigger the sanity check would look like this:

#include <iostream>
#include "scpp_assert.hpp"

using namespace std;

int main(int argc, char* argv[]) {
  cout << "Hello, SCPP_ASSERT" << endl;

  try {
    double stock_price = 100.0;   // Reasonable price
    SCPP_ASSERT(0 < stock_price && stock_price <= 1e6,
      "Stock price " << stock_price << " is out of range.");

    stock_price = -1.; // Not a reasonable value
    SCPP_ASSERT(0 < stock_price && stock_price <= 1e6,
      "Stock price " << stock_price << " is out of range.");

  } catch (const exception& ex) {
    cerr << "Exception caught in " << __FILE__ << " #" << __LINE__ << ":
"
         << ex.what() << endl;
  }

  return 0;
}

Running this example leads to the following output:

Hello, SCPP_ASSERT Exception caught in
      scpp_assert_exception_test.cpp #20: SCPP assertion failed with message
      'Stock price -1 is out of range.' in file scpp_assert_exception_test.cpp
      #17.

Note that here we also receive additional information—not only where the error has occurred but also where it was caught, which could be a useful hint when trying to figure out what exactly happened before involving a debugger.

Another question is why we need to call a SCPP_AssertErrorHandler function located in a separate scpp_assert.cpp file instead of doing the same thing inside the macro in the scpp_assert.hpp file. The short answer is that debuggers usually prefer to step through the functions as opposted to stepping through macros. We’ll return to this subject in Chapter 15.

Now we have two macros: one to use in production and one for testing only. When should you use each one? As the author of your program, only you can answer this question. Typically, you should have a feeling for how often the function that will contain a sanity check called, how long it takes to execute, and how long the evaluation of the sanity check will take as compared to the execution of the function itself.

If you know that the function is called rarely or maybe even just once for initialization purposes, and the sanity checks are cheap, then go ahead and use the permanent macro. You might be glad you did when a problem is reported from the field. In other cases, use the temporary macro.

Note that when evaluating how long the sanity check takes, all that matters is how long it takes to evaluate the Boolean condition. How long it takes to compose a message is not relevant: if you get to that stage, you are in no rush at all.

Different sanity checks slow down your program to different extents. One of the worst in this regard, the index-out-of-bounds sanity check, will be discussed in Chapter 4. So you might add some more granularity to this process and define different macros for different types of bugs, if some of them are slowing testing too much. Feel free to experiment with what works best for your code.

We now have macros that allow us to write sanity checks easily and still compose a meaningful error message. When do we write them? If you think: “I will write my code and then return and add sanity checks,” chances are it will never happen. Also, while you are writing your code, the picture of what is going on in it and which conditions should be true or false is in the freshest possible state in your brain. So the answer is to write sanity checks while you are writing the code. Any time you can think of any condition you can check for—write a sanity check for it. Even better, when you start writing a new function, start with writing sanity checks for all inputs before you write anything else.

“But this is a lot of additional work!” you might think. True, but as we’ve seen, writing sanity checks is not difficult, and more importantly it will pay off later at the testing stage. It is much easier to write sanity checks while you have a mental picture of the algorithm in your head than have to go back and debug the code later.

In Part II, we’ll consider some of the most common mistakes in C++ code and learn how to deal with them—one at a time.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.33.235