Making the Calculator More General with a “Little Language”

You need to make a little change to the user interface of the calculator, to make it more “testable.”

User Interface

The part of the program that sends output to the user and gets input from the user.


Instead of prompting users to input numbers or indicate that they want to stop, the program will now get commands from a “little language.” These commands will be typed into the running program when it needs input—just as you type on your handheld calculator.

A statement in this language has the following form:

<operator><operand>

<operator> can be any of the following:

+ to add the operand to the accumulator

- to subtract the operand from the accumulator

* to multiply the accumulator by the operand

/ to divide the accumulator by the operand

@ to set the accumulator to a specific value

= to show the current value in the accumulator

? to show the tape

! to have the calculator test itself

. to stop the program

An entire set of operators and operands can be entered on a single line (for example, +3-2*12/3=, which outputs 4). Note that our little language has no precedence and performs its steps strictly from left to right (0+3 = 3; 3-2 = 1; 1*12 = 12; 12/3 = 4).

An operand is optional for the =, ?, !, and . operators.

Changes to main()

The main() function has a few changes, as shown in Listing 14.1.

Listing 14.1. main() with Changes
  1: int main(int argc, char* argv[])
  2: {
  3:    SAMSErrorHandling::Initialize();
  4:
 *5:    char Operator; // Used in the loop
  6:
  7:    do
  8:    {
  9:       try
 10:       {
 11:          Operator = GetOperator();
 12:
*13:          if
*14:             (
*15:                Operator == '+' ||
*16:                Operator == '-' ||
*17:                Operator == '*' ||
*18:                Operator == '/' ||
*19:                Operator == '@' // Set value
*20:             )
*21:          {
 22:             float Operand = GetOperand();
 23:             Accumulator(Operator,Operand);
 24:          }
*25:          else if (Operator == '!')
*26:          {
*27:             SelfTest();
*28:          }
*29:          else if (Operator == '.')
*30:          {
*31:             // Do nothing, we are stopping
*32:          }
*33:          else // Some other operator, no operand
*34:          {
*35:             Accumulator(Operator);
*36:          } ;
 37:       }
 38:       catch (runtime_error RuntimeError)
 39:       {
 40:          SAMSErrorHandling::HandleRuntimeError (RuntimeError);
 41:       }
 42:       catch (...)
 43:       {
 44:          SAMSErrorHandling::HandleNotANumberError();
 45:       } ;
 46:    }
*47:    while (Operator != '.'), // Continue
 48:
 49:    Tape('.'), // Tell the tape we are terminating
 50:
 51:    return 0;
 52: }
					

Line 5 moves the variable Operator outside the loop because its value will be used to stop the loop, as seen in line 47.


Lines 13–21 identify and get the operands for operators that have them, and then give them to the accumulator.

You can see that the name of the Accumulate() function has been changed to Accumulator(). Naming a function with a noun instead of a verb indicates that it has an internal state and that it does not depend solely on the arguments passed in any specific call.

Lines 25–28 run the self-test.

Line 29 makes sure that the program does nothing when the . operator is entered. The empty block makes this clear.

Lines 33–36 are for any operator with no operand. This call on the Accumulator() has only one parameter.

Accumulator() Changes

The Accumulator() shown in Listing 14.2 has some new lines in its switch statement, because a much smaller percentage of its operators are recorded on the Tape().

Listing 14.2. Accumulator() Implementing New Operators
 *1: float Accumulator (const char theOperator,const float theOperand = 0)
  2: {
  3:    static float myAccumulator = 0;
  4:
  5:    switch (theOperator)
  6:    {
  7:       case '+':
  8:
  9:          myAccumulator = myAccumulator + theOperand;
*10:          Tape(theOperator,theOperand);
 11:          break;
 12:
 13:       case '-':
 14:
 15:          myAccumulator = myAccumulator - theOperand;
*16:          Tape(theOperator,theOperand);
 17:          break;
 18:
 19:       case '*':
 20:
 21:          myAccumulator = myAccumulator,theOperand;
*22:          Tape(theOperator,theOperand);
 23:          break;
 24:
 25:       case '/':
 26:
 27:          myAccumulator = myAccumulator / theOperand;
*28:          Tape(theOperator,theOperand);
 29:          break;
 30:
*31:       case '@':
*32:
*33:          myAccumulator = theOperand;
*34:          Tape(theOperator,theOperand);
*35:          break;
 36:
*37:       case '=':
*38:          cout << endl << myAccumulator << endl;
*39:          break;
 40:
 41:       case '?':  // Display the tape
 42:          Tape(theOperator);
 43:          break;
 44:
 45:       default:
 46:          throw
 47:             runtime_error
 48:                ("Error - Invalid operator");
 49:    } ;
 50:
*51:    return myAccumulator;
 52: }
					

Line 1 has one of the most substantial changes. It now has the Accumulator() returning the current value of myAccumulator on line 51.


There is a new feature shown in the formal argument theOperand on line 1. An equal sign and a zero follow the formal argument name. This means that the formal argument is an optional argument, and if the actual argument is not provided in a function call, the argument will be given a default value—in this case 0. The default value lets the compiler know that the call in line 49 of main() is allowed.

The other new lines are straightforward—they simply add calls to Tape() for every operator whose action is to be recorded, or implement new operators, such as setting the accumulator value on lines 31–35 and displaying the accumulator value on lines 37–39.

The Input Function Changes

Getting the operator and operand requires a minor change—as you can see in Listing 14.3, the prompt has been removed.

Listing 14.3. GetOperator() and GetOperand() Without the Prompt
 1: char GetOperator(void)
 2: {
 3:    char Operator;
 4:    cin >> Operator;
 5:
 6:    return Operator;
 7: }
 8:
 9: float GetOperand(void)
10: {
11:    float Operand;
12:    cin >> Operand;
13:
14:    return Operand;
15: }

The SelfTest Function

The SelfTest function (see Listing 14.4) is performed by line 27 of main() when you enter ! as input. It runs a test of the accumulator.

Listing 14.4. SelfTest()
 1: void SelfTest(void)
 2: {
 3:    float OldValue = Accumulator('='),
 4:
 5:    try
 6:    {
 7:       if
 8:          (
 9:             TestOK('@',0,0) &&
10:             TestOK('+',3,3) &&
11:             TestOK('-',2,1) &&
12:             TestOK('*',4,4) &&
13:             TestOK('/',2,2)
14:          )
15:       {
16:          cout << "Test completed successfully." << endl;
17:       }
18:       else
19:       {
20:          cout << "Test failed." << endl;
21:       } ;
22:    }
23:    catch (...)
24:    {
25:       cout << "An exception occured during self test." << endl;
26:    } ;
27:
28:    Accumulator('@',OldValue);
29: }

This function is wrapped in a try/catch, so if the test encounters a problem that throws an exception, the function can restore things to the state they were in before the test started. It may also catch errors resulting from the use of heap allocation in Tape(). But remember that heap allocation errors can do so much damage that an exception may never be thrown.


Line 3 saves the value of the accumulator, which is restored in Line 28.

Lines 7–14 actually execute the tests. This block tests every operator that changes the accumulator by calling the TestOK() function. Because this uses the relational operator &&, if any tests fail, the self-test fails.

The TestOK() Function

SelfTest() uses TestOK() (see Listing 14.5) to determine whether each operator/operand submitted to the Accumulator() provides the expected result.

Listing 14.5. TestOK()
 1: bool TestOK
 2: (
 3:    const char theOperator,
 4:    const float theOperand,
 5:    const float theExpectedResult
 6: )
 7: {
 8:    float Result = Accumulator(theOperator,theOperand);
 9:
10:    if (Result == theExpectedResult)
11:    {
12:       cout << theOperator << theOperand << " - succeeded." << endl;
13:       return true;
14:    }
15:    else
16:    {
17:       cout <<
18:          theOperator << theOperand << " - failed. " <<
19:          "Expected " << theExpectedResult << ", got " << result <<
20:          endl;
21:
22:       return false;
23:    } ;
24: }

This is a function that has a bool result and three const arguments—the operator, the operand, and the result expected from the accumulator. The function performs the operation and reports whether or not the result matches what is expected.

Test functions like this and SelfTest() must be kept simple. You need to be able to validate test functions by eye (by performing what is called a walkthrough). If they are wrong, you might hunt for bugs (runtime errors) that don't exist or miss ones that do.

A Slight Change to the Tape()

You only need to change one line in the Tape() function to aid testing.

Convert

*3:    static const int myTapeChunk = 20;

to

*3:    static const int myTapeChunk = 3;

Why? To verify that heap allocations are working, testing must provoke Tape() to resize the tape at least once. This is done by making the chunk size smaller than the number of operations in the test.

Running the Program

It's time to look for errors. Let's run a self-test:


1: !
2:
3: 0
4: @0 - succeeded.
5: +3 - succeeded.
6: -2 - succeeded.
7: *4 - failed. Expected 4, got 1
8: Test failed.

Test line 1 shows the self-test operator being input to the program—type it and press Enter. Line 3 shows the value of the accumulator being saved by the test, as a result of line 3 in SelfTest(). The tests succeed until output line 7.

Notice that only three of the four tests have run. Why didn't the division test run?

Short-circuit Evaluation

One of the nice things about the && relational operator is that it uses what is called short-circuit evaluation. This means that as soon as any expression connected by && is false, subsequent expressions will not be executed. And why should they? As soon as any expression is false, the entire expression will be false.

Further tests are not performed if any of them fail, because each test's expected result requires a correct result from the prior test. But short- circuit evaluation can be a problem if you expect a function in such an expression to execute regardless of the outcome of prior expressions.

What Was Wrong?

Fortunately, the self-test points you right to the error line:

19:       case '*':
20:
21:          myAccumulator = myAccumulator,theOperand;

You will notice that the program is not multiplying in line 21. Instead of a *, there is a ,. This was not a contrived error. I actually made this error while writing the example and discovered it in the self-test.

You might wonder why the compiler lets you make this mistake. It turns out that the , is the infix comma operator. It returns the value of the rightmost operand as its result. There are some uses for this, but it is very uncommon.

Fix the Error and Rerun

Once you've fixed the error and rerun the program, you will see that the program works fine. Even the Tape() reallocations seem to have no problem. But be skeptical. Heap allocation errors can hide in even the best code and not show up in testing.

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

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