C++ Beginners


:: Session 4 ::

Chapter 5: Errors | 1/2


Programming, Principles and Practice Using C++ — by Bjarne Stroustrup

A program…


  1. should produce the desired results for all legal inputs
  2. should give reasonable err messages for all illegal inputs
  3. need not worry about misbehaving hardware
  4. need not worry about misbehaving system software
  5. is allowed to terminate after finding an error

3 Approaches


  1. Organize software
  2. Eliminate most of the errors through debugging and testing
  3. Make sure the remaining errors aren't serious

Avoid → Find → Correct

source of errors — check-list


  • Poor Specification: not knowing exactly what that program should do
  • Incomplete: not handle all the cases
  • Unexpected Arguments: e.g. sqrt(-1.2); will give you a domain error and "NaN" will be returned
  • Unexpected Inputs
  • Unexpected State: i.e. data 'state' (see Chapter 26!)
  • Logic Errors

compile-time errors


Errors found by compiler.

Before generating code, compiler analyzes code to detect sytax- & type errors.

It only allows to proceed when everything conforms to the language specification.

syntax errors


The reports of Sytax Errors are offen cryptic.

Tip: also look at previous lines in the messages

type errors


Reports of the mismatches between the types declared for the variables, functions etc. and the types of values or expressions assigned to or passed as arguments.

For example:


int area(int length, int width) {...;} // calculate area
int x0 = arena(7); // err: undeclared function
int x1 = area(7);  // err: wrong number of arguments*
int x2 = area("seven", 2); // err: 1st argument has wrong type!
*: every function call must provide the expected number of arguments, of the right types, and in the right order !: and it can't implicitly convert string to integer

non-errors


remaining errors like:


  int area(int length, int width) 
    {...;} // calculate area of a rectangle
  int x3 = area(10, -7);  
    // OK. but rectangle with a width of minus 7?
  int x4 = area(10.7, 9.3); // OK. but calls area(10, 9) instead
  char x5 = area(100, 999); // OK. but truncates the result
              

link-time errors


Rules:

  1. Every function in a program must be declared with the same type in every translation unit.
  2. Every function must be defined exactly once.

The rules hold also for all other entities of a program, such as variables & types.

Concider the following code:


  int area(int length, int width) {...;} // our area() function
  double area(double length, double width) {...;} // not our area()
  int area(int x, int y, char unit) {...;} // not our area()
              

run-time errors


Errors found by checking in a running program.

What to do with an error caught at run time? Questions:

  • Is an erroneous result acceptable?
  • If not, who should detect the errors:
    the caller or the callee?
  • And how should such errors be reported?

For example:


int area(int length, int width) { 
  return length*width; } // calculate area of a rectangle
constexpr int frame_width = 2;
int framed_area(int x, int y) {
  return area(x-frame_width, y-frame_width); 
} // calculate area within frame
int main() {  int x=-1, y=2, z=4;
  int area1 = area(x, y);
  int area2 = framed_area(1, z);
  int area3 = framed_area(y, z);
  double ratio = double(area1)/area3;
  //convert to double to get floating-point divsion
}
              

The caller deals with errors

…when we can't modify the called function, e.g. in a library.

Catch runtime error: write out an error message or take other actions if error found

Protecting the call of area(x,y) in main() completly:


int main() {  int x=-1, y=2, z=4;
  if(x<=0 || y<=0 ) 
    error("non-positive area() argument");
  int area1 = area(x, y);
  if(1-frame_width<=0 || z-frame_width<=0 ) 
    error("non-positive argument for area() called by framed_area()");
  int area2 = framed_area(1, z);
  if(y-frame_width<=0 || z-frame_width<=0 ) 
    error("non-positive argument for area() called by framed_area()");
  int area3 = framed_area(y, z); }
              

The callee deals with errors

Check for valid arguments within the called functions

Arguments-hecking code is in one place, and that one place is exactly where the arguments are to be used


int area(int length, int width) {   // calculate area of a rectangle
  if(length<=0 || width<=0 ) error("non-positive area() argument");
  return length*width; }
int framed_area(int x, int y) {
  constexpr int frame_width = 2;
  if(x-frame_width<=0 || y-frame_width<=0 ) 
    error("non-positive argument for area() called by framed_area()");
  return area(x-frame_width, y-frame_width); }  // calculate area within frame
int main() {
  int x=-1, y=2, z=4;
  int area1 = area(x, y);
  int area2 = framed_area(1, z);
  int area3 = framed_area(y, z); }
              

Can't use callee to handle errors when…

  • Can't modify the function definition
  • The called function doesn't know what to do in case of error. (typically functions in libraries)
  • The called function doesn't know where it was called from
  • Performance

Error Reporting

Exceptions


To separate detection of an arror (should be done in a called function, aka callee) from the handling of an error (should be done in calling side of function).

  1. A function finds an error and throws an exception indicating what went wrong.
  2. Any direct or indirect caller can catch the exception, i.e. specify what to do if the callee used throw.
  3. A function expresses interest by using a try-block listing the kinds of exceptions it wants to handle in the catch parts of the try-block.
  4. If no caller catches an exception, the program terminates.

exception basic examples

1. Bad Arguments Errors

Callee side:


class Bad_area {}; // a type specifically for reporting errors from area()
int area(int length, int width) {
  if(length<=0 || width<=0 ) {
    throw Bad_area{}; // throw a Bad_area exception in case of bad argument
  }
  return length*width; 
}
              

1. Bad Arguments Errors

Caller side:


int main() 
try { int x=-1, y=2, z=4;
      ...
      int area1 = area(x, y);
      int area2 = framed_area(1, z);
      int area3 = framed_area(y, z); 
      double radio = area1/area3;
    }
catch (Bad_area) {
      cout<<"Oops! Bad arguments.\n";
}
              

2. Range Errors

"Collection of data": containers

The most common and useful standard library container is the vector

The general notation [low:high) e.g. [0:v.size()), means indices from "low" to "high-1", i.e. including low but not high

Trying to read v[v.size()] which is one beyond the end of the vector, is an "off-by-one" error, or a "range error"


  for(int i=0; i<=v.size(); i++) {}
              

2. Range Errors

vector's subscript operation: vector::operator[] reports finding an error by throwing an exception (of type: out_of_range).


  int main() 
  try { vector<int> v;
        for (int x; cin>>x;)
          v.push_back(x);
        for (int i=0; i<=v.size(); ++i)
          cout<<"v["<<i<<"]=="<<v[i]<<'\n';
      }
  catch (out_of_range) {
        cerr<<"Oops! Range error.\n";
        return 1;
      }
  catch (...) {  // catch all other exceptions
        cerr<<"Exception: something went wrong.\n";
        return 2;
      }
              

3. Bad Input

Test if the last input operation succeeded by:


  double d = 0;
  cin >> d;
  if(cin) {
    // all is well, and we can try reading again
  }
  else {
    // the last read didn't succeed, so we take some other action
  }
              

3. Bad Input

We (just) want to report the error and terminate the program


  double sfn() {
    double d = 0;
    cin >> d;
    if(!cin) error("couldn't read a double in 'sfn()'");
    // later we'll come back and do something more useful
  }
              
error() can't return a value. and… is supposed to terminate the program after getting its message written.

3. Bad Input

Another type of exceptions defined by standard library is runtime_error(s)


  void error(string s) { throw runtime_error(s); }
  int main()
  try { // ...our program...
        return 0;  // 0 indicates success
      }
  catch (runtime_error& e) {
      cerr>>"runtime error: ">>e.what()>>'\n';
      keep_window_open();
      return 1;  // 1 indicates failure
      }
              

3. Bad Input

  • The e.what() extracts the error message from the runtime_error.
  • The & in (runtime_error& e) is an indicator of "passing the exception by reference."
  • The cerr is like cout, except it's meant for error output. (can also diverted to a different target other than to the screen in other operating systems, e.g. to a file.
  • The type exception is a common base (supertype) of type out_of_range and type runtime_error
  • error() is commonly used to give two pieces of information passed along to describe the problem, by concatenating the strings in it:
  • 
      void error(string s1, s2) { throw runtime_error(s1+s2); }
                  

4. Narrowing Errors (implicitly truncated)

Write a function that tests and throw a runtime_error exception if an assignment or initialization would lead to a changed value. e.g.:


  int x1 = narrow_cast<int> (2.9);  // throws
  int x2 = narrow_cast<int> (2.0);  // ok.
  char c1 = narrow_cast<char> (1066);  // throws
  char c2 = narrow_cast<char> (85);  // ok.
              
  • The <...> are the same as are used for vector<...>.
  • The <...> are used when we need to specify a type rather than a value.
  • A "cast" doesn't cange its operand, it produces a new value of the type specified in the <...> that corresponds to its operand value.