6. Writing a Program

"Programming is understanding." -- Kristen Nygaard

6.1 A problem

We want the problem we take on next to have the following characteristics:

  • Illustrates good design and programming techniques
  • We explore the kind of decisions we make in designing "real" programs.
  • Doesn't require too many new programming languages features.
  • Is complicated enough to require thought about its design.
  • Allows for many variations in its solution.
  • Solves an easily understood problem.
  • Solves a problem worth solving.
  • Has a small enough solution to fully comprehend.

Our author chose programming a calculator as our first multi-part program.

6.2 Thinking about the problem

We start by thinking about the problem. An important point: Stroustrup recommends discussing the problem with a friend. Programming is a social activity.

The technique of development here progressively refines an initial cut at the problem. This is the method that has the best track-record for achieving good results. In today's jargon, it is often called "developing a minimal viable product" (MVP) to start with, and then continually improving that product.

6.2.1 Stages of development

Working on a problem, we typically repeatedly go through these stages:

  • Analysis: What does a solution to our problem look like? What features must it include? What features would be nice to include?
  • Design: How do we break the system into comprehensible parts to achieve the goals analysis has revealed to us?
  • Development: Write the code that implements our design, and debug and test it.
6.2.2 Strategy

Some suggestions:

  • Take the user's point of view and ask what the program is for. If you were using it, how would you like it to work?
    • Is the problem clear? Are we asking for too much? (Best to build an MVP, and then improve it based on user feedback.)
    • Can we really tackle this project, given our time, resources, knowledge, etc.? No point in taking on something you can't possibly complete!
  • Break the program down into manageable parts.
    • Can we use existing tools, libraries, etc. that might help? If so, always use them! How do we know if they are worth adopting? Ask friends, ask Google, look on StackOverflow.
    • Look for parts of the solution that can be separately described and potentially re-used. Correctly identifying such parts requires experience: that is why we present you with many examples.
  • Build a small, limited version of the program that solves a key part of the problem.
    Why?
    • This will force to the surface misapprehensions in our understanding of the problem we wish to solve. Ultimately, the computer itself is the measure of our design!
    • We also may need to change some aspects of the design to make our problem manageable: perhaps the "ideal" spec involves using an exponential algorithm, but in fact we can come "close enough" with a polynomial one.
  • Build a full-scale version, using parts of the initial version. Our ideal: Grow a program from working parts.
6.3 Back to the calculator!

We're not going to try to learn GUI programming in this course, so let's stick to the console, using cout and cin. We want to be able to take as input, say:
2+2
and return 4 as a result. Or we should be able to accept:
2+2*3 and return 8 as a result.

We may first come up with some pseudo-code like:
read_a_line
calculate
write_result

6.3.1 First attempt

Well, we are anxious to start writing code, so let us try:

#include "std_lib_facilities.h"

int main()
{
    cout <<"Please enter an expression (we can handle + and -):\n";
    int lval = 0;
    int rval;
    char op;
    int res;
    cin >> lval >> op >> rval; // read e.g. 1 + 3

    if(op=='+')
        res = lval + rval;
    else if(op=='-')
        res = lval - rval;

    cout << "Result: " << res << '\n';
    keep_window_open();  // if necessary on your system!
    return 0;
}
                            

lval is our variable for the left-hand value, and rval for the right-hand value.
Well, we have something (kind of) working!
Let's improve our program by:

  1. Clean up our code
  2. Add multiplication and division
  3. Add the ability to handle more than one operand (e.g., 1+2+3)

So let's try this:

#include "std_lib_facilities.h"

int main()
{
    cout << "Please enter an expression (we can handle +, -, *, and /):\n";
    cout << "Add an x to end expression (e.g., 1+2*3x): ";
    int lval = 0;
    int rval;
    int res;
    cin >> lval;
    if(!cin) error("No first operand!");
    for(char op; cin >> op;) {   // repeatedly read ops
        if(op!='x') cin >> rval;
        if(!cin) error("No second operand!");
        switch(op) {
        case '+':
            lval += rval;
            break;
        case '-':
            lval -= rval;
            break;
        case '*':
            lval *= rval;
            break;
        case '/':
            lval /= rval;
            break;
        default:
            cout << "Result: " << lval << '\n';
            keep_window_open();  // if necessary on your system!
            return 0;
        }
    }
    error("Bad expression");
}
                            

Problems:

  1. We accept multi-line expressions.
  2. Our operator precedence is wrong! But how do we find the "correct" operator amongst all input?
  3. How do we remember the place of each op?
  4. How do we avoid proceeding strictly left-to-right?
6.3.2 Tokens

We are going to tokenize our input to get this right! Token will be our first user-defined type. We will initially define:

  • Floating point numbers: 1.23, 0.274e2, 42
  • Operators: +, -, *, /, %
  • Parentheses: (, )

Tokens will hold a kind and a value (for floats).

6.3.3 Implementing tokens

Our initial definition of Token:

class Token {
public:
    char kind;
    double value;
};
                            

We can initialize (construct) Token objects like this:

Token t1 {'+'};
Token t2 {'8', 11.5};
                            

6.3.4 Using tokens

We will read a stream of tokens into a vector, so we need:

Token get_token();  // read token from cin

vector<Token> tok;

int main()
{
    Token t = get_token();
    tok.push_back(t);
}
                            

6.3.5 Back to the drawing board

OK, we have tokens: now what? This helps us sorting out the text into pieces, but how do we get the order of evaluations correct? And can we support variables? We'd better back up and think some more.

6.4 Grammars

How do we get an expression like 45+11.5/7 correct? We tokenize it to:

45
+
11.5
/
7
                        

Then we write a grammar to express our rules like "division binds tighter than addition" and write code to implement that grammar. For example:

// a simple expression grammar:

Expression:
    Term
    Expression "+" Term
    Expression "-" Term
Term:
    Primary
    Term "*" Primary
    Term "/" Primary
    Term "%" Primary
Primary:
    Number
    "(" Expression ")"
Number:
    floating-point literal
                        

Let's parse some expressions using this grammar:
2
2+3
45+11.5*7

6.4.1 A detour: English grammar

To wrap our heads around this, let's make a simplistic grammar for English:

Sentence:
    Noun Verb
    Sentence Conjunction Sentence
Conjunction:
    "and"
    "but"
    "or"
Noun:
    "birds"
    "fish"
    "C++"
Verb:
    "rules"
    "fly"
    "swim"
                            

We can now make sentences like "C++ rules but fish swim."

6.4.2 Writing a grammar

A grammar for lists:

List:
    "{" Sequence "}"
Sequence:
    Element
    Element "," Sequence
Element:
    "A"
    "B"
                            

According to that grammar, which of the following are valid lists, and which not?

{ A }
{ }
A, B, A, B
{ A, B, B, B }
{ A, B, B, B, }
{ A B B B }
                            

6.5 Turning a grammar into code

6.5.1 Implementing grammar rules

We are going to try to turn our grammar rules into working C++ code. I will offer you pseudo-code for each of the elements of the grammar in what follows. What we will need to implement this grammar is:

  1. A way to get the next token of input.
  2. A function to implement the expression rule: expression().
  3. A function to implement the term rule: term().
  4. A function to implement the primary rule: primary().

So let's look at pseudo-code version of 2-4: we will create a new class to handle 1.

6.5.2 Expressions

Expression pseudo-code:

double expression()
{
    left = term();
    Token t = get_the_next_token(); // we will see how to do this later!
    while(true) {
        switch(t.kind) {
            // here we need cases to handle + and -
            // the default case will return left
        }
    }
}
                            

6.5.3 Terms

Term pseudo-code:

double term()
{
    double left = primary();
    Token t = get_the_next_token(); // we will see how to do this later!
    while(true) {
        switch(t.kind) {
            // here we need cases to handle * and /
            // the default case will return left
        }
    }
}
                            

6.5.4 Primary expressions

Primary pseudo-code:

double primary()
{
    Token t = get_next_token; // we will see how to do this later!
    switch (t.kind) {
    case '(':    // handle '(' expression ')'
        {    
// here we will expect an expression, and then a ')':
// you must write this switch so as to handle that
            return d;
        }
    case '8':            // we use '8' to represent a number
        return t.value;  // return the number's value
    default:
        error("primary expected");
}

                            

6.6 Trying the first version

I'm going to let you try to implement the pseudo-code of the final version. Nevertheless, looking at the textbook's code for the first version is a useful exercise.

6.7 Trying the second version

I'm going to let you try to implement the pseudo-code of the final version. Nevertheless, looking at the textbook's code for the second version is a useful exercise.

6.8 Token streams

Token_stream will allow us to push back tokens when we have read one too many in our parsing efforts.

6.8.1 Implementing Token_stream

Here is an implementation of Token_stream:

class Token_stream {
    public:
        Token get();
        void putback(Token t);
    private:
        bool full{false};
        Token buffer;
};

void Token_stream::putback(Token t)
{
    buffer =t;
    full = true;
}

Token Token_stream::get() {
    if(full) {
        full = false;
        return buffer;
    }
    char ch;
    cin >>: ch;
    switch(ch) {
        case ';':
        case 'q':
        case '(': case ')': case '+': case '-': case '/': case '*': case '%':
            return Token{ch};
        case '.':
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
        {
            cin.putback(ch);
            double val;
            cin >>: val;
            return Token{'8', val};
        }
        default:
            return Token{'i', double(ch)};
    }
    return Token{'q'};
}
                            

6.8.2 Reading tokens

Most tokens we recognize by simply seeing the character that represents that token.

6.8.3 Reading numbers

However, when we see a digit, we want to take advantage of the C++ facility to read a double for us: thus, we push the digit we read back onto cin, and then try to read a double from cin.

6.9 Program structure

The structure of the whole program (with details omitted) should be:

#include "std_lib_facilities.h"

class Token { /* ... */ };
class Token_stream { /* ... */ };

void Token_strem::putback(Token t) { /* ... */ }
Token Token_stream::get() { /* ... */ }

Token_stream ts;
double expression();

double primary() { /* ... */ }
double term() { /* ... */ }
double expression() { /* ... */ }

int main() { /* ... */ }
                        

Order is important! Names must be declared before they are used.

Test Yourself!
  1. List some ways to break a program into smaller, more manageable parts?
    1. classes and functions
    2. classes and control structures
    3. if and while statements
    4. variables and functions
  2. What are the three main phases of software development?
    1. analysis, design and implementation
    2. classes, functions, and variables
    3. hacking, coding, and testing
  3. If we input (3+7)/(8-2) to the calculator, then (3+7) is a
    1. expression
    2. term
    3. primary
    4. number
  4. We typically break an interpreter into two phases called
    1. expressing and optimizing
    2. tokenization and compiling
    3. tokenization and parsing
    4. parsing and expressing
  5. In the switch() in the expression function, why is the default to "put back" the token?
    1. That token will probably be a + or - and can be dealt with by the next call to expression.
    2. If we hit the default case, we have read one token too many.
    3. That token will be meaningless, and we want to throw it away.
  6. A primary purpose of classes is to
    1. make our program more classy
    2. organize our code better
    3. make our program more object-oriented
  7. Classes include
    1. header files and libraries
    2. methods and header files
    3. variables and methods
  8. What is a "use case"?
    1. It helps in checking the usability of the program at different situations
    2. It is a term that describes how a user can make a program re-usable
    3. It is a term that describe how a user uses a system to accomplish a particular goal
  9. What is a token?
    1. a variable
    2. Smallest element of a program meaningful to the compiler
    3. a constant
    4. Smallest element of a program
  10. Why do we want to rely on libraries of code?
    1. It helps in improving your codeing skills
    2. It saves time, uses pre tested code and use modular code
    3. We should not rely on them and should instead write our own code
  11. Which is not a type of access modifier in C++?
    1. Private
    2. Public
    3. Protected
    4. Personal
Answers

1. a; 2. a; 3. c; 4. c; 5. b; 6. b; 7. c; 8. c; 9. b; 10. b; 11. d;

Drill