2

I thought I understood handling bad input with cin.clear() and cin.ignore(), like it is explained here, but in the following example

#include <iostream>
#include <limits>
using namespace std; //I know that this isn't good practice.

int main () {
    int a, b;

    while (cout << "Input some int: " && !(cin >> a)) {
        cout << "Wrong datatype!\n";
        cin.clear();
        cin.ignore(numeric_limits<streamsize>::max(), '\n');
    }

    while (cout << "Input some int: " && !(cin >> b)) {
        cout << "Wrong datatype!\n";
        cin.clear();
        cin.ignore(numeric_limits<streamsize>::max(), '\n');
    }

    if (a > 1) cout << "Some event.\n";
    if (b > 1) cout << "Some other event.\n";

    return 0;
}

the behavior I want is only present when the unwanted input is some character. So if I enter x and y, I will again be asked for two ints and get the appropriate outputs, same if I enter a char and an int two times.

However: If I input, say, 2.3, I will get

Input some int: Wrong datatype!

but won't have a chance to correct my input, since the result invariantly outputs "Some event." The second prompt just accepts the float right away.

Cœur
  • 37,241
  • 25
  • 195
  • 267
erdapfel
  • 21
  • 2

5 Answers5

2

What's happening, actually, is the 2 in 2.3 is being accepted by the first prompt, leaving .3 in the input buffer. The Wrong datatype! you are seeing is from your second prompt, seeing a ., which is not a valid character for an integer. You then, I assume, enter an integer which is accepted by your second prompt.

Benjamin Lindley
  • 101,917
  • 9
  • 204
  • 274
1

The problem here is when you enter something like 2.3 to a int cin is okay with that. It reads the 2, sees the . so it stops reading and stores the 2 in the variable and leaves the .3 in the buffer for the next call. So, you pass the first loop, get to the second loop, and then you fail as it tries to read in the . into b. Then you clear the .3 and you can enter another input. If you enter another 2.3 the same thing will happen and b will get 2 and the program continues on.

The "bullet proof" way to read in input is to read it in as a std::string and then parse that to make sure the full input was good. That would look like

std::string line;
while (cout << "Input some int: " && std::getline(cin, line)) {
    std::stringstream ss(line);
    ss >> a;
    if (ss.eof()) // we did consume all the input
        break;
    else
        cout << "Wrong datatype!\n";
}

while (cout << "Input some int: " && std::getline(cin, line)) {
    std::stringstream ss(line);
    ss >> b;
    if (ss.eof()) // we did consume all the input
        break;
    else
        cout << "Wrong datatype!\n";
}
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • No, this solution is not good at all. It is very inefficient to do an unformatted input by cin, just to perform another, formatted input by stringstreams. You should rather read the int and then check by cin.get() whether the next character is actually a newline. If not, there was something unacceptable in the stream. – Jodocus Oct 20 '17 at 12:40
  • @Jodocus You would have to test for every white space character, not just newline. Instead of writing all that code I chose to take the little more expensive path that I know can't break. If you're waiting for user input you can spare a "few" cycles to do it this way. – NathanOliver Oct 20 '17 at 12:43
  • There is no problem to add a condition || std::isspace() there as well. – Jodocus Oct 20 '17 at 12:48
1

This fundamental approach is fragile, and error-prone.

Your obvious intent is to accept a line of input, and process it. If so, then the correct function to do that is std::getline(). That's what its purpose is. That's exactly what it does. The >> operator does not do that. That's not what it's for. Of course, by using the various auxiliary methods, like ignore(), and clear(), one can still achieve that goal, but, as you've discovered, using those functions correctly is not intuitive. Of course, you can spend copious time pouring over their documentation to understand their every semantic behavior, but why bother, when you can simply use std::getline(), and then move on to something else. It's simply easier to do that.

Of course, once a line of input is received, you would like to parse it into an integer. Now is the correct time to use >> to parse it:

std::string line;

if (std::getline(line, std::cin))
{
    std::istringstream i{line};

    int n;

    if (i >> n)
    {
           // Input parsed
    }
}

Isn't this simpler, more straightforward, and less of a gotcha?. Of course, entering "2.3" here will result in the >> operator parsing the "2", and succeeding, leaving ".3" unparsed. If you would like to detect this situation, simply use get() to see what's left in the std::istringstream. Perhaps accept any trailing whitespace, if you wish.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • This is not the way C++ I/O streams are supposed to be used. You can directly read an integer from cin and check via cin.get() whether the next char read is '\n'. If not, there was an unacceptable character in the stream before the user hit enter. Using strings and stringstreams together with getline is very inefficient an the reason many people say C++ I/O is slow - it's not. But it's not used correctly. – Jodocus Oct 20 '17 at 12:44
  • 1
    Except that checking cin.get() is not sufficient. You also have to check if the formatted input operation has failed. Then, you have to clear the error condition. Then, you may or may not have to discard unprocessed input. Suddenly, things are not as straightforward as it appears to be at first, and you quickly get lost in the weeds. The task at hand is not "to read an integer from cin", but to "read a whole line from cin, and validate that it contains a single integer". Big difference. And if performance is an issue, you wouldn't use streams in the first place. – Sam Varshavchik Oct 20 '17 at 15:50
  • @ Sam: Of course. I have written an example in my answer that does all that. Don't get me wrong, I am no big fan of C++ I/O streams either, it is an 80's design with odd mechanics. But the morale should be: either use it right or do not use it at all. – Jodocus Oct 20 '17 at 16:11
  • Your example, @Jodocus, appears to accept input with trailing whitespace which will not get discarded, remain unread, and screw up input after the next prompt to the user, if each input is individually prompted, as the original question does.Thank you, for proving my point. – Sam Varshavchik Oct 20 '17 at 16:17
  • It does only because someone wanted it to accept white-space separated input. Remove the isspace condition and it will not allow trailing whitespace. Also, your code accepts trailing whitespace as well. And it accepts input like "4 5", ignoring the 5 and reading 4. This is not desired, either. Also, trailing whitespace will not remain in the stream. The sentry object of the next >> operation will read and discard it properly. – Jodocus Oct 20 '17 at 16:22
  • I discussed the issue with whitespace in my answer, after showing the code. As I pointed out, it is natural to accept spurious trailing whitespace, if only whitespace follows valid input on the line. Only whitespace: good. Whitespace followed by something else on the line: bad. – Sam Varshavchik Oct 20 '17 at 16:26
  • Yeah your code does just exactly that. It accepts "1 2", reading 1 and discarding 2. The 2 will not be even considered when the next line is read, it's just discarded. In my code, it will remain in the stream to be read by the next operation. – Jodocus Oct 20 '17 at 16:28
  • 1
    There must be some bug on stackoverflow.com that keeps you from seeing my comments, that follow the code. – Sam Varshavchik Oct 20 '17 at 16:29
  • Whole point is: in order for your code to achieve what mine does is reading out get() - exactly what I did, but without any additional unnecessary stringstreams. – Jodocus Oct 20 '17 at 16:31
0

When you input "2.3", cin will stop at '.', and interpret '2' as the desired input.

Then, you will clear cin, when the '.' is encountered, discarding 3.

If you then input a new integer, it will accept it.

gsamaras
  • 71,951
  • 46
  • 188
  • 305
-1

Many answers here suggest the use of std::getline and string parsing, either using the string functions or stringstreams. This is quite inefficient and not the way the streams are supposed to be used.

Instead, parse the data when it is still in the input stream:

#include <iostream>
#include <cctype>
#include <limits>

struct read_int {
    int& a;

    read_int(int& aa) : a{ aa } { }
    friend std::istream& operator >>(std::istream& is, read_int& ri) {
        char delim;
        while(!(is >> ri.a) || (delim = is.get(), delim != '\n' && !std::isspace(delim))) {
            std::cerr << "Bad!\n";
            is.clear();
            is.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
        }
        return is;
    }
};


int main() {
    int a, b;
    std::cin >> read_int(a) >> read_int(b);
    std::cout << a << ' ' << b;
    return 0;
}

This function will accept input like "4 5" or "4\n6" alike, but requests a new input for data like "4.2", discarding everything read before.

Jodocus
  • 7,493
  • 1
  • 29
  • 45