7. Completing a Program

7.1 Introduction

Now that we are "done" with our program, let's actually complete it!

7.2 Input and output

Let's add a simple prompt for input: >

7.3 Error handling

Now we should try to break our program. Let's generate many instances of "bad" input, like:

  • 4 + + ;
  • 6 + ( 4 * ) - 2 ;
  • 3 ;;;;;
  • (1+);

Try "ridiculous" input that no one should try: foozziefabblebop. Our book author used to feed his emails into the compiler.

7.4 Negative numbers

Our program does not handle negative numbers properly. It is typical that we only find out if our design is correct by coding it: the computer is the only real test as to whether the design works. To implement unary minus (and plus -- might as well!) we change our grammar for primary to:

Primary:
    Number
    "(" Expression ")"
    "-" Primary
    "+" Primary
                        

Changing the code accordingly is so simple it might just work the first run!

7.5 Remainder: %
Adding 4 to 9, in 12-hour-clock arithmetic, yields 1.

Adding modulo (%) should be easy: we add it as a Token, and define a meaning for it.

The meaning for integer operands is clear and unambiguous. But what about for floating point numbers? There is no perfect answer. But often, modulo for floats is defined as:
x%y==x-y*int(x/y)
The standard library function fmod() from <cmath> implements that definition. So let's modify term() to include:

case '%':
    {
        double d = primary(ts);
        if (d == 0) error("divide by zero");
        left = fmod(left, d);
        t = ts.get();
        break;
    }
                        

As our textbook notes, we could have also decided to forbid non-integer args to modulo.

7.6 Cleaning up the code

We should never consider our code done until it is in a state where we could hand it off to someone else for maintenance, without having to rely on us being by their side to understand what is going on.

7.6.1 Symbolic constants

We can make a big step forward in both the readability and maintainability of our program by defining some symbolic constants. In particular, we will want them for '8', ';', and 'q', for input, and for '>' and '=' for output;

7.6.2 Use of functions

Our functions should name and isolate logically separate parts of our program. Most of our functions so far are pretty good in this respect: expression(), term(), and primary() directly express our grammar, get() handles fetchijng tokens from our input. But at this point main() both provides the "scaffolding" for the whole program, and also handles the calculation loop. Let's divide that into two functions to reflect this logical division.

void calculate(Token_stream& ts)
{
    while(cin) {
        cout << prompt;
        Token t = ts.get();
// this output is for debugging:
//          cout << "in main(), got token: " << t.kind
//              << " with val of " << t.value << '\n';
        while(t.kind == print) t = ts.get();
        if(t.kind == quit) return;
        ts.putback(t);
        cout << result << expression(ts) << '\n';
    }
}
                            

Drill

Write the baove function in your calculator source code file, and then modify your main() to that it uses that function. (This is called re-factoring your code.)

7.6.3 Code layout

The way we format our code can make a big difference as to its readability. For instance, while C++ compilers don't care aout indentation, "proper" indentation can make a huge difference to us as human readers of code. Our textbook author suggests we re-organize Token_stream::get() so that the first set of cases are on separate lines.

7.6.4 Commenting

Write comments only to express the intentions of your code that you can't make obvious by the code itself. For instance, never write:

int x = a + b;  // add a and b and assign the result to x
                            

7.7 Recovering from errors

"Quality error handling is one mark of a professional." -- Bjarne Stroustrup

Instead of exiting on an error, let's clean up our mess and get ready to accept more input! Here is our first try at cleaning up:

void clean_up_mess()   // naive version
{
    while(true) {      // skip until we hit a print
        Token t = ts.get();
        if(t.kind == print) return;
    }
}
                        

Nice try, but not quite there. To see why, try:
1@z; 1+3;
A better approach is to build this into Token_stream:

void Token_stream::ignore(char c)
    // c is token kind to look for
{
    // first check buffer:
    if(full && c == buffer.kind) {
        full = false;
        return;
    }
    full = false;

    // now search input:
    char ch = 0;
    while(cin >> ch)
        if(ch == c) return;
}
                        

Remember: this must also be declared in the class interface!

7.8 Variables

7.8.1 Variables and definitions

7.8.2 Introducing names

7.8.3 Predefined names

7.8.4 Are we there yet?

Test Yourself!
  1. Why did we introduce a const name like 'number' into our program?
    1. to make the program more maintainable
    2. to make it easier to change the char for a number
    3. all answers are correct
    4. to make the program more readable
  2. When should you start to test your program?
    1. only when done coding
    2. testing isn't really necessary
    3. almost as soon as you've started coding
  3. When should you retest?
    1. Only after major changes.
    2. After every change, no matter how small.
    3. One round of testing is enough.
  4. What is wrong with commenting x = a + b; with // add a and b and put value in x
    1. It does not describe the code correctly.
    2. Variable names should never occur in comments.
    3. It will confuse the compiler.
    4. It only says what the code already clearly says.
  5. What is the purpose of commenting in a program?
    1. It is necessary for the compilation of the code
    2. None of the above
    3. It is something not to be bothered about
    4. To make the program more readable and easy to understand
  6. What is the use of symbolic constant?
    1. Nothing much useful in it
    2. The code is less error prone, easier to change and understand
    3. None of the above
    4. Symbolic constants help the code to run faster
Answers

1. c; 2. c; 3. b; 4. d; 5. d; 6. b;

Drill