#054 – Input validation in C++, Part 2: making std::cin bulletproof – Part 2

#053 – Input validation in C++. Why std::cin >> x is fragile – Part 1 catalogued the three ways std::cin >> x can fail: the stream entering a fail state, leftover characters in the buffer, and semantically invalid input.

By the end of the Nibble, our simple calculator program don’t crash with invalid input.

Fix 1: Semantically invalid input

Consider the case where we enter t as the operator

Enter a decimal number: 12
Enter one of the following: +, -, *, or /: t
Enter a decimal number: 7
12 t 7 is 

Rather than failing, the program happily goes forward. The result is 12 t 7 is. Whilst the program succeeded in extracting the input, the output is meaningless.

Let’s refactor getOperator(), adding a while (true) and a switch/case block. Now, if the user fails to enter a valid operator, it will repeatedly ask them until they do.

// Before
char getOperator()
{
    std::cout << "Enter one of the following: +, -, *, or /: ";
    char op{};
    std::cin >> op;
    return op;
}

// After
char getOperator()
{
    while (true) // Keep looping until we get a valid input
    {
        std::cout << "Enter one of the following: +, -, *, or /: ";
        char op{};
        std::cin >> op;

        // Validate the input using a switch case
        switch(op)
        {
            case '+':
            case '-':
            case '*':
            case '/':
                return op; // If valid exict the white (true)
            default: // In the case of invalid input
                std::cout << "Please enter a valid operator\n";
        }
    }
}

Let’s enter h as the operator.

The program doesn’t proceed, rather it informs us to enter a valid operator. Nice!

Enter a decimal number: 12
Enter one of the following: +, -, *, or /: h
Please enter a valid operator
Enter one of the following: +, -, *, or /:

NOTE: The empty-case-fallthrough idiom was covered in #049 – Using [[fallthrough]] to strengthen switch/case blocks. It makes the list of valid operators read as a single block, and the default case handles everything else. The loop only exits when op is one of the four expected characters.

2nd fix: Leftover characters

If we enter 5*7 as the decimal number, the program auto completes. It doesn’t prompt us for the operator or the 2nd number.

Enter a decimal number: 5*7
Enter one of the following: +, -, *, or /: Enter a decimal number: 5 * 7 is 35 

This leads us to our 2nd error case. The extraction succeeds but the program rolls over.

What we want is for std::cin to only extract 5.

What happens is that std::cinn encounters an invalid character (*) and fails silently.

When std::cin is invoked again, prompting the user to enter an operator, *7 is available in the buffer so it automatically uses that. It extracts *. The 3rd std:cinn sees 7 in the input buffer and uses it.

To clear the buffer, let’s use a helper function, ignoreLine(). This discards everything in the input buffer up to and including the next newline.

std::numeric_limits<std::streamsize>::max() is the largest value the stream can use as a count, which effectively means “ignore without limit until you hit the delimiter.

// Before
double getDouble()
{
    std::cout << "Enter a decimal number: ";
    double x{};
    std::cin >> x;
    return x;
}

// After 
#include <limits>
    // for std::numeric_limits

// Helper function to clear the buffer
void ignoreLine()
{
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
double getDouble()
{
    std::cout << "Enter a decimal number: ";
    double x{};
    std::cin >> x;

    // Clear the buffer
    ignoreLine(); 
    return x;
}
// Output
Enter a decimal number: 5*7
Enter one of the following: +, -, *, or /: *
Enter a decimal number: 7
5 * 7 is 35

Now if we type 5*7 only 5 gets extracted and the remaining characters get discarded, leaving the buffer clean for the next extraction. We’ll call ignoreLine() after we invoke std::cin.

Perfect. Now the program progresses line by line.

3rd error case

What happens if we enter a as a decimal number?

Enter a decimal number: a
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator
Enter one of the following: +, -, *, or /: Please enter a valid operator

If we enter a, the program fails and enters an infinite loop. The program fails to extract the input. This is the 3rd error case.

Let’s examine why. When a is placed in the input buffer, >> attempts to extract it to double x. Since this type conversion cannot occur, a is left in the buffer and std::cinn enters failure mode. In this mode, future extractions silently fail, causing the infinite loop seen above.

Let’s fix the stream. Here’s how:

  1. Detect the failure with if (!std::cin).
  2. Clear the fail state and restore std::cin with std::cin.clear().
  3. Flush the bad input with ignoreLine()

Here is the refactored getDouble()

double getDouble()
{
    while (true) {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        if (!std::cin) {                // extraction failed
            std::cin.clear();           // restore the stream
            ignoreLine();               // discard the bad input
            std::cout << "Invalid number. Try again.\n";
            continue;
        }

        ignoreLine();                   // discard trailing newline
        return x;
    }
}

Now, the program will not proceed unless we enter a number. Perfect!

Enter a decimal number: a
Enter a decimal number: b
Enter a decimal number: t
Enter a decimal number: 12
Enter one of the following: +, -, *, or /: 

NOTE: if (!std::cin) calls the stream’s operator!, which returns true if either the fail bit or bad bit is set. It is shorthand for if (std::cin.fail()) and is the conventional way to test a stream after an extraction.

In Conclusion

As you code your programs, consider how users will misuse your program. Since we cannot restrict what the user can type, the program must be robust to handle any sort of input.

Consider:

  • Could extraction fail?
  • Could the user enter more input than expected?
  • Could the user enter meaningless input?
  • Could the user overflow an input?

Appendix – The Complete Program

#include <iostream>
#include <limits>

void ignoreLine(){
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

double getDouble(){
    while (true) {
        std::cout << "Enter a decimal number: ";
        double x{};
        std::cin >> x;

        if (!std::cin) {
            std::cin.clear();
            ignoreLine();
            std::cout << "Invalid number. Try again.\n";
            continue;
        }

        ignoreLine();
        return x;
    }
}

char getOperator(){
    while (true) {
        std::cout << "Enter one of +, -, *, /: ";
        char op{};
        std::cin >> op;
        ignoreLine();

        switch (op) {
            case '+': case '-': case '*': case '/':
                return op;
            default:
                std::cout << "Invalid operator. Try again.\n";
        }
    }
}

void printResult(double x, char op, double y){
    std::cout << x << ' ' << op << ' ' << y << " is ";
    switch (op) {
        case '+': std::cout << x + y << '\n'; return;
        case '-': std::cout << x - y << '\n'; return;
        case '*': std::cout << x * y << '\n'; return;
        case '/': std::cout << x / y << '\n'; return;
    }
}

int main(){
    double x{ getDouble() };
    char   op{ getOperator() };
    double y{ getDouble() };
    printResult(x, op, y);
}