10
\$\begingroup\$

This program will validate whether a mathematical expression entered by the user is a valid expression based off whether the expression itself has been entered with the correct scope openers and scope closers. The program will then send the results to a text file. The program will not compute the mathematical expression (that will come later when i have this part down)

#include<iostream>
#include<cctype>
#include<fstream>
#include<cstdlib>
#include<istream>
using namespace std;

struct NodeType;
typedef NodeType* Nodeptr;
typedef char StackType;

class Stack
{
    public:
           bool IsEmpty()const;
           bool IsFull() const;
           void Push(StackType newItem);
           StackType StackTop() const;
           void Pop();
           Stack();
           ~Stack();

    private:
            Nodeptr top;
};

struct NodeType
{
    StackType item;
    Nodeptr link;
};

// Function declarations 
void describeProgram();
void read_expression(char expression[80]);
void check_expression(char expression[80], bool& valid); 
void report_result(char expression[80], bool valid);
bool another();

int main()
{
    char expression[80];  //Expression to be read from the keyboard
    bool valid;           //true id expression is valid, false otherwise  

    do
    {
        read_expression(expression);
        check_expression(expression, valid);
        report_result(expression, valid);
    }while(another());

    system("PAUSE");
    return 0;
}


Stack::Stack()
{
   top = NULL;
}

Stack::~Stack()
{
   Nodeptr p;
   while(top != NULL)
   {
      p = top;
      top = top->link;
      delete p;
   };
   cout<<"Stack has been reallocated to the heap."<<endl;
   return;
}

bool Stack::IsEmpty() const
{
   return (top == NULL);
}

bool Stack::IsFull() const
{
    int* p;
   return (!(p = new int));
}

void Stack::Push(StackType newItem)
{
   Nodeptr q;
   q = new NodeType;
   q->item = newItem;
   q->link = top;
   top = q;
   return;
}

StackType Stack::StackTop() const
{
   return top->item;
}

void Stack::Pop()
{
   Nodeptr p;
   p = top;
   top = top->link;
   delete p;
   return;
}

void describeProgram()
{ 
    cout << "This program prompts the user for a mathematical expression. After,"
            "the program will check to see whether the expression is valid or "
            "invalid. After its validity is confirmed or denied, the program will"
            " let the user know. The user will be asked if they want to repeat "
            "the program.";     
}

void read_expression(char expression[80])
{
    cout << "Enter your mathematical expression: \n";
    cin.ignore();
    cin.getline(expression, 80);

    while(expression == NULL)
    {
        cout << "Please enter a valid expression: \n";
        cin.ignore();        
        cin.getline( expression, 80);
    }
    return;
}

void check_expression(char expression[80], bool& valid)
{
    Stack symbStack;  
    char symbol, top_symb;  
    int i = 0; 

    valid = true;  
    symbol = expression[i];

    while(symbol != '\0') 
    {
        if( symbol == '{' || symbol == '[' || symbol == '(' )
        {
            symbStack.Push(symbol);
        }

        else if( symbol == '}' || symbol == ']' || symbol == ')' )
        {
            if(symbStack.IsEmpty())
            {
                cout << "Expression is invalid! \n";
                valid = false;
                return;
            }

            else
            {
                top_symb = symbStack.StackTop();
                symbStack.Pop();
            }    

            if(  (top_symb != '(' && symbol == ')') ||  
                 (top_symb != '[' && symbol == ']') ||
                 (top_symb != '{' && symbol == '}')  )
            {
                valid = false;
            }
        }
        i++;  
        symbol = expression[i]; 
    }

    if(!(symbStack.IsEmpty()))
        valid = false;  
    return;     
}

void report_result(char expression[80], bool valid)
{
    ofstream toFile;
    toFile.open("scope.txt",ios::out);

    if(!valid)
    {
        cout << "\n The expression: " << expression << " was an invalid expression \n";
        toFile << "\n The expression: " << expression << " was an invalid expression \n";
    }

    else
    {
        cout << "\n The expression: " << expression << " was a valid expression \n";
        toFile << "\n The expression: " << expression << " was a valid expression \n";
    }
    toFile.close(); 

    return;
}


bool another()
{
    char response;
    cout << "\n Do you wish to run this program again (Y or N) \n";
    cin >> response;     

    while((response!= 'N') && (response!= 'Y') && 
          (response!= 'y') && (response!= 'n'))
    {
        cout << "Please enter a valid response";
        cin >> response;
    }

    response = toupper(response);

    return response == 'Y';
}
\$\endgroup\$
10
\$\begingroup\$

Don’t write using namespace std;.

You can, however, in a CPP file (not H file), or inside a function, put individual using std::string; etc. (See SF.7.)


Did you know that there is a stack container adaptor in the standard library? And basing it on a linked list is slow — use a vector.

I'll note some things about the Stack code for general teaching, since you should actually not write any of that at all.

Stack::Stack()
{
   top = NULL;
}

Don’t assign values in the body of the constructor; use an initializer list. (C.49)

This one could be a default initializer (with the top field definition) and then you don’t write the constructor at all. (C.48)

But you should not have naked new (and delete). So top should actually be a unique_ptr which knows how to initialize itself so you don’t need to do anything. (R.11)

And, don’t use the C NULL macro.


char expression[80];  //Expression to be read from the keyboard

Don't hard-code array sizes or use them like this (R.14). The function should be written as

std::string read_expression()

check_expression(expression, valid);

No, return results. And declare variables where used and initialized:

bool valid = check_expression(expression);

Using “out” parameter instead of returns is bad; for no reason is just horrible. (See F.20.)

Your parameters taking arrays probably don't mean what you think. In any case, pass std:string (or string_view which can be especially handy for parsing).


return (top == NULL);

Don’t use the C NULL macro (ES.47). But, don’t write explicit tests against nullptr. (ES.87) Use the contextual conversion to bool, which works efficiently for raw pointers and any kind of smart pointer instance.


bool Stack::IsFull() const
{
    int* p;
   return (!(p = new int));
}

That makes no sence! You leak memory, and always return true since new throws an exception on failure. How can a linked list be full, anyway?


It’s good to use typedefs instead of the actual type, for clarity in reading the code as well as for ease in changing things.

typedef char StackType;

But, typedef is obsolete and limited. Today, write instead (T.43)

using StackType = char;

But don’t (generally) typedef pointers to other things. Read and understand something* as a pointer to something, not needing another unique name like somethingpointer instead.


You don’t need a return at the end of a void function.

\$\endgroup\$
6
\$\begingroup\$

I’ll go over check_expression itself, the meat of your logic, in a separate message. Your other functions are just incidental and you just need to learn what’s already in the library and the usual good practices.

Stack symbStack;  
char symbol, top_symb;  
int i = 0; 

valid = true;  
symbol = expression[i];

while(symbol != '\0') 

Good in defining symbStack inside the function (not as a global).

You should define symbol at first use and initialize it; and having top_symb (needs to be remembered across loop iterations) is awkward.


Your loop is awkward, in that symbol and i=0 is set before beginning the loop, and updated at the end. This should really be a for loop, and symbol moved inside:

const auto max = strlen(expression);
for (i=0; i < max; ++i) {
    auto symbol= expression[i];
      ⋮

But really, don’t iterate using i index! You really want the symbol and don’t care about i at all. So write:

for (auto symbol : expression) {
    ⋮

and that’s so much clearer and direct!

Back to dealing with top_symb — instead of remembering it between loop iterations, record what you pop off the stack, and only pop when you detect a closing-type symbol, so you know it must in fact be popped. It will either match, or signal an error so you don’t care anymore.


It would be handy to have a simple function to classify characters as opening, closing, or not interesting.

Also, move the matching of the proper open-close pair to another helper function.

Here is my rewrite:

bool check_expression (string_view s)
{
    std::stack <Symbol_type, std::vector<Symbol_type>> openstack;

    for (auto const ch : s) {
        switch (classify(ch)) {
            case boring:   break;
            case open_delim:
                openstack.push(ch);
                break;
            case close_delim:
                if (openstack.empty())  return false;  // close with no opens
                if (openstack.top() != match_for(ch))  return false;  // wrong kind of close
                openstack.pop();
                break;
        }
    }

    return openstack.empty();
}

Even without seeing the various helpers it calls, you should understand it. Writing this first and then getting those helpers to work as smaller problems is Top-Down Design.

Here is the whole program which you can save, compile, and run.

The stuff at the beginning of the file:

#include <string>
#include <iostream>
#include <string_view>
#include <vector>
#include <stack>

using std::string_view;

using Symbol_type = char;  // abstract this out to make it easy to change it to Unicode
   // and more easily illustrate in the code where variables are being used for this purpose.

Now I define my matches.

// Use tables of data and a loop, not repeated code with slightly different names each time.
// this also makes it easy to *extend*, and it is clear that the logic for matching open/close
// is right since it’s in one place.

constexpr std::pair<Symbol_type,Symbol_type> matchers[] = {
    {'(',')'},  {'[',']'},  {'{','}'}
};

Note that each symbol is only typed once, and the way they pair up is listed in the data. Suppose the next order is to have a version that includes angle brackets as delimiters — just update the table; no code to change at all. Making it have different modes that are selected with a parameter would only be a little more work (throwing everything in a class and making the table a class member).

Now the helpers themselves, that use this data to know what to do:

// simple functions that use this chart to know the results

enum charkind { boring, open_delim, close_delim };

charkind classify (Symbol_type c)
{
    for (auto [left,right] : matchers) {
        if (c == left)  return open_delim;
        if (c == right)  return close_delim;
    }
    return boring;
}


Symbol_type match_for (Symbol_type c)
{
    // too much trouble to set up std::find … a for loop is so easy,
    // and can start by copying the previous function and just change the return values!

    for (auto [left,right] : matchers) {
        if (c == left)  return right;
        if (c == right)  return left;
    }
    throw std::invalid_argument(__FUNCTION__);
}

So simple! It doesn’t matter how many sets of different delimiters we define, the code is only doing the test once, in general. There can only be three cases: the passed-in symbol matches the left, matches the right, or does not match anything. So, the function is only 4 lines long including a line for the loop.

The check_expression function is next in the file, and you already saw that. I define the functions in order of dependency, so there are no forwarding declarations. So, main is at the bottom, the simplest helpers at the top.

I finish off the file with some testing. I did not prompt the user for input or whether he wants to run it again; just called the function a number of times with some test cases. In “real life” this is the way it works: you have unit tests that can be run automatically and contain carefully considered cases to check for.

The programmer who ordered this function might end up using it wherever, so it does not deal with input or output: it is given parameters and returns results. For example, it may be used in a form validator, not a console program.

void test (int id, string_view s)
{
    bool OK = check_expression (s);
    std::cout << id << " - " << std::boolalpha << OK << '\n';
}


int main()
{
    test (1, R"Q(constexpr std::pair<Symbol_type,Symbol_type> matchers[] = {
    {'{',')'},  {'[',']'},  {'{','}'}
    };
    )Q");

    test (2, "testing.");
    test (3, "[ / {{{ } ");
    test (4, "this) is an empty (stack)");
}

Et Voilà!


Notes

for (auto symbol : expression) assumes that expression has been changed to a string, string_view, or somesuch. As originally written, the parameter (written as an array but really a pointer) will not work, but boost::as_literal(s) will turn a nul-terminated C string into a range.

\$\endgroup\$
  • \$\begingroup\$ is () a valid expression? \$\endgroup\$ – papagaga May 3 '18 at 11:16
  • \$\begingroup\$ @papagaga it is according to his specs. \$\endgroup\$ – JDługosz May 3 '18 at 21:57
2
\$\begingroup\$
  • Some inconsistencies with your indentation which should be fixed.

  • Don't use using namespace std

  • Prefer prefix over postfix

  • The NodeType struct can be part of the stack.

  • Using a fixed-length expression limit is asking for trouble. (Enter something longer than 80 characters and see what happens)

  • system("PAUSE") is not supported on all platforms.

  • You have a function that returns bool, takes it by reference and another one takes it by value. This makes for a confusing interface. On first glance people might think you forgot to pass it by reference in the second function.

  • return at the end of a void function is not needed.

  • return 0 at the end of main is redundant

  • The use of raw pointers is discouraged. You should try to use smart pointers whenever possible.

  • I'm confused by the IsFull() method. As of now it's never called. It just checks if you can allocate memory (presumably checking if there is any free memory left). Also what happens if it succeeds? When do you relase the memory? If you're concerned about memory then the check should be done when you allocate a new node.

\$\endgroup\$
2
\$\begingroup\$

Tokenizer, lexer, parser...

NB: There are already three good reviews of your code, so I will only comment on the broader view.

The program will not compute the mathematical expression (that will come later when i have this part down)

I'm not sure you're doing it in the right order. When you think of it, there are a lot of common parts in validating an expression and computing its result. For instance, you need first to make "words" out of a stream of characters, like integers, operators, delimiters, etc. Those words are generally called tokens, and the tool for doing it a tokenizer*. A simple example:

tokenize("(12+3.14)") => ["(", "12", "+", "3.14", ")"];

You'll also need to add meaning to the tokens. "3.14" isn't a floating point number, nor ")" a closing parenthesis -matching opening and closing symbols isn't possible until that meaning is assigned to certain symbols. Symbols tagged with their category are called lexemes, and categorizing the tokens is the job of the lexer:

lex {"(", "12", "+", "3.14", ")"}) => [ {"(", OP}, {"12", INT}, ... , ")", CP}]

You'll then need to organize the lexemes, be it to validate the expression or evaluate it. It means you need to divide the expression in sub-expressions, and decide on an order of evaluation / validation. The fact that you evaluate separately a sub-expression between parenthesis and the fact that an closing parenthesis must match an opening one are linked. The organization of the lexemes is the task of the parser:

parse ({ {"(", OP}, {"1", INT}, {"+", ADD}, {"1", INT}, {")", CP}, {"*", MUL}, {"3", INT} }
=>                 [*]
            |              |
           [+]            [3]
       |         |
      [1]       [1]

Is all that necessary?

Is it really necessary to distinct those three steps? Well, it isn't necessary to translate them into separate steps in your code for the simplest cases; the first two steps are frequently combined. But you have to keep them in mind, because they'll help you organize your code way better. Your current distinction between validating and evaluating is artificial. You'll go through the same steps -tokenizing, lexing, parsing- for both.

A quick and dirty example

Here's a quick attempt at a mathematical expression evaluator, which separates lexing (no need to differentiate lexing and tokenizing in this instance) and parsing which have both responsibilities for validating and evaluating in their respective perimeters: https://wandbox.org/permlink/nOz5wJ0IL8nHYMp3

*the terminology varies: tokenizer vs scanner, lexer vs tokenizer, etc.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.