1

I've been learning C++, and this chunk of code is from a simple grading program. But when I try to get the user input, there's a problem. If I enter a number, whether it be less than 0 or more than 100, or anything in between, my loop works fine. But if I type in any letter, or any non-alphanumeric character (ex: +, (, %, etc.) I get an infinite loop with "Please enter a grade value between 0 and 100" printed forever. What am I doing wrong?

Thanks.

int _tmain(int argc, _TCHAR* argv[])
{
using namespace std;

int grade = -1; // grade will hold grade value; initialized to -1
do {
    cout << "Please enter a grade value between 0 and 100." << "\n";
    cin >> grade;
} while (grade < 0 || grade > 100);

cout << grade << "\n";

printGrade(grade);

return 0;
}
taocp
  • 23,276
  • 10
  • 49
  • 62
  • 2
    Well, if you read in something that isn't a number, what are you expecting to happen? – Dennis Meng Aug 08 '13 at 19:53
  • Presumably, he correctly expects that `grade` will remain unchanged, and that the loop will continue, and then (incorrectly) that he can try to enter data again. – Mooing Duck Aug 08 '13 at 19:55
  • check this out? http://stackoverflow.com/questions/5131647/why-would-we-call-cin-clear-and-cin-ignore-after-reading-input – taocp Aug 08 '13 at 19:56
  • Wouldn't any letter be converted to the ascii equivalent? So if I typed 'a', why doesn't it get stored as 65 (or whatever the respective ascii code is)? – NewtoProgramming Aug 08 '13 at 19:56
  • No, letters in a stream are not converted to their ASCII equivalent. – Mooing Duck Aug 08 '13 at 19:57
  • I actually tested this and I never get an infinite loop as I expected. What compiler are you using? – khajvah Aug 08 '13 at 19:57
  • @MooingDuck I am using gcc and I never get an infinite loop, what bug can I have? – khajvah Aug 08 '13 at 20:01
  • @MooingDuck So the problematic code has no problems and "solved" answers just ruined the working code... GREAT! :D – khajvah Aug 08 '13 at 20:05
  • @khajvah: No wait, I'm still testing, you're right that this code isn't showing an infinite loop, but I can't figure out why. It _ought_ to loop infinitely by my understanding of the spec... (But yes, the answers totally infinite loop) – Mooing Duck Aug 08 '13 at 20:07
  • oh hey, `cin >> grade;` sets `grade` to `0` when there's an error, I didn't know that happened. – Mooing Duck Aug 08 '13 at 20:07
  • 1
    http://stackoverflow.com/questions/17430495/effects-on-input-variable-after-failed-input-stream – Neil Kirk Aug 08 '13 at 20:10
  • @MooingDuck Right, I thought it sets to ASCII equivalent of entered char but seems it sets to 0 when the variable is int... this was helpful – khajvah Aug 08 '13 at 20:11
  • 2
    "You get zero because you have a pre-C++11 compiler. Leaving the input value unchanged on failure is new in the latest standard." – Neil Kirk Aug 08 '13 at 20:11

4 Answers4

2

if cin>>grade fails (aka can't parse as int) it does not consume the stream. You can try:

int main()
{ using namespace std;

int grade = -1; // grade will hold grade value; initialized to -1
do {
    cout << "Please enter a grade value between 0 and 100." << "\n";
    if (!(cin >> grade))
    {
      cin.clear();
    }
} while (grade < 0 || grade > 100);

cout << grade << "\n";

return 0;

}

But this is only part of the problem. Really, you should use std::getline and parse the grade as a full line for correct input.

IdeaHat
  • 7,641
  • 1
  • 22
  • 53
  • This _also_ has an infinite loop, though in a rare corner case that is unrelated to the OP's problem. – Mooing Duck Aug 08 '13 at 19:58
  • @MooingDuck could you elaborate? – Neil Kirk Aug 08 '13 at 19:58
  • [A picture is worth a thousand words](http://coliru.stacked-crooked.com/view?id=a1482b3fcbc0ed8321c9ee419b1e276b-cc73e281b3b6abc9bf6cf4f153b944a6) (EOF) – Mooing Duck Aug 08 '13 at 19:59
  • @MooingDuck Either that doesn't work on my browser or I just don't understand it – Neil Kirk Aug 08 '13 at 20:01
  • @NeilKirk: The code is in the top pane, and the output results are in the bottom pane. [Here it is on ideone.com](http://ideone.com/sfwuBk) – Mooing Duck Aug 08 '13 at 20:02
  • @MooingDuck Very well, how to fix it? – Neil Kirk Aug 08 '13 at 20:05
  • @NeilKirk: Easiest thing to do is ignore the rest of the line. Harder but arguably more correct would be to spin through the input buffer until you find a whitespace character. – Mooing Duck Aug 08 '13 at 20:32
  • @NeilKirk As mentioned in my answer, use std::getline to consume the full line, *then* attempt to parse it. This also solves the problem that the user can input a grade like "10+50" which will get parsed as 10 and leave "+50" in the cin buffer, another big no-no. – IdeaHat Aug 08 '13 at 20:41
1

If cin does not receive valid input for the data type (int), the variable grade is not changed and remains at -1. You can test whether the input was successful like so

bool success = (cin >> grade);
if (! success)
{
    cin.clear();
    cout << "bad input\n";
    break;
}

You can also use this as a shortcut if (! (cin >> grade))

Note that you need to clear the error state of cin before you use it again.

Neil Kirk
  • 21,327
  • 9
  • 53
  • 91
  • 1
    Additionally the character stays on the input stream, so the next loop iteration immediately processes the character again. You may also want to flush/clear the input stream versus break from the loop as described above. – R Dub Aug 08 '13 at 19:58
  • @NeilKirk: `clear` clears the _state_ but not the _content_, but does not ignore the invalid character, so you'll get an infinite loop as well if you aren't careful. – Mooing Duck Aug 08 '13 at 20:01
  • @MooingDuck What should I do? These vague statements are not helpful. If I knew enough to understand what you are eluding to, I wouldn't have made the mistake. – Neil Kirk Aug 08 '13 at 20:02
  • 1
    @NeilKirk: Oh, I was slightly vague here because the code here doesn't itself cause a bug, but the most likely context one would put it in _would_ be a bug. Also, there's another quirk in his code that's causing problems which you make no mention of (namely that when cin>>grade fails, grade is set to zero) – Mooing Duck Aug 08 '13 at 20:13
1

I'm pretty sure the cin failed so you may need to reset its fail flag or something like that.

Add this to your loop:

if (cin.fail())
{
    cout << "failed";
    cin.clear();
}
MasterPlanMan
  • 992
  • 7
  • 14
1

Correctly and safely reading until you get valid input is far trickier than you'd think. If there's invalid input, like a letter, the stream is set in a "failure" state, and refuses to read any more characters until you clear the state. But even if you clear the state, that input is still waiting there, in the way. So you have to ignore those characters. The easiest thing to do is simply ignore everything until the next enter key, and then try the input again.

But it gets even more complicated, because if the stream has an error, it gets set in a "bad" state, or if it reaches the end of the stream, it gets set in a "eof" state. Neither of these two are recoverable, so you must detect them and quit the program to avoid an infinite loop. Even more irritating, istreams have a .fail() function, but that checks if it's in fail or bad, which makes it nearly useless in my opinion. So I wrote a little invalid_input which checks if the stream can continue.

Note that get_grade sets the fail flag manually if the input is out-of-range.

#include <iostream>
#include <stdlib.h>
#include <limits>

bool invalid_input(std::istream& in)
{return in.rdstate() == std::ios::failbit;}

std::istream& get_single_grade(std::istream& in, int& grade) {
    std::cout << "Please enter a grade value between 0 and 100." << "\n";
    if (in>>grade && (grade<0 || grade>100))
        in.setstate(std::ios::failbit);
    return in;
}

bool get_grade(std::istream& in, int &grade) {
    while(invalid_input(get_single_grade(in, grade))) { //while we failed to get data
         in.clear(); //clear the failure flag
         //ignore the line that the user entered, try to read the next line instead
         in.ignore(std::numeric_limits<std::streamsize>::max(),'\n'); 
    }
    return in.good();
}

int main(int argc, char* argv[]) {    
    int grade = -1; // grade will hold grade value; initialized to -1
    if (get_grade(std::cin, grade) == false) {
        std::cerr << "unxpected EOF or stream error!\n";
        return false;
    }    
    std::cout << grade << "\n";
    return EXIT_SUCCESS;
}

As you can see here, this doesn't get into an infinite loop when given out of bounds numbers, end of file, a stream failure, or invalid characters.

Mooing Duck
  • 64,318
  • 19
  • 100
  • 158