Let's say you're making a simple console program where you ask the user for input and then your program does stuff. (This is a common scenario for people being taught C++ in school.) The most obvious way to get input, and the way that most people are taught to get input, is to use something called the formatted extraction operator, aka operator>>
.
Unfortunately, it's wrong. There are a multitude of issues with getting input this way:
ignore
ing everything the user has typed before doing anything further.ignore
everything currently waiting in the input stream, or risk getting the wrong input for the next part of your program.ignore
everything else because the formatted input operators do not read anything after their input, including newline characters. So, the user types a number and presses enter to say "I'm done". Your program reads that number but does not read the newline after it. This is fine as long as the next input operation is also formatted - the newline character will be read and discarded. But, what if the next input operation is to read an entire line? Well, you'll get an empty line before the user even gets a chance to type anything.That's not to say that formatted input is useless, just that it shouldn't be used in many cases. There are good uses cases for formatted input, as we will see shortly, but front-line user input is not one of them.
Let's solve this problem. Recall that the user presses enter when they have finished giving input - a majority of consoles wait until this moment to start sending input to your program. That means that at the end of each user input is a newline character. So? Let's start there: read entire lines at a time, to guarantee that you and the user agree on what was inputted.
#include <iostream> | |
#include <string> | |
int main() | |
{ | |
std::string line; //stores the most recent line of input | |
while(std::getline(std::cin, line)) //read entire lines at a time | |
{ | |
//deal with the contents of `line` | |
} | |
} |
Why is std::getline
inside the loop as a condition? This is standard good practice - looping on the input/output operation. Most stream I/O operations return the stream itself, and the stream itself is implicitly convertible to a boolean which is true
when the stream is in a good state, and false
otherwise. This is useful when reading from streams that eventually end, such as files. (It is possible for the console input to end too, but that's usually a mistake on the user's part bar special circumstances).
Okay, great - now we can get each line of input separately. But, what if we didn't want an entire line of input? What if we just wanted a number? Well, reading entire lines helps to avoid the issues mentioned above, so we still want that. What we want now, is a way to get the number out of that string.
#include <iostream> | |
#include <sstream> | |
#include <string> | |
int main() | |
{ | |
std::string line; | |
int num; | |
//split onto multiple lines for readability | |
while((std::cout << "Please enter a number: ") //make sure the user knows what you expect | |
&& std::getline(std::cin, line) | |
&& !(std::istringstream{line} >> num) //construct a stringstream from `line` and read it into `num` | |
) //this loop continues on bad input and ends on good input | |
{ | |
std::cerr << "Invalid input, try again." << std::endl; //let the user know they made a mistake | |
} | |
//hooray, we can use `num`! | |
} |
std::istringstream
is an input stream (just like std::cin
), and since it is completely independent from std::cin
, we can feel free to screw it over and let bad things happen to it. That means we aren't ruining our one and only source of input - instead, we read entire lines and then proxy them through a temporary input stream so that bad things have no permanent effects.
However, the above code will still accept input such as "123abc" - the difference is that "abc" will be thrown away and not left in the input buffer to strike down your next input operation. If you want "123abc" to count as an invalid input, all you need to do is ensure that you can't read from the temporary stream after reading the number:
#include <iostream> | |
#include <sstream> | |
#include <string> | |
int main() | |
{ | |
std::string line; | |
int num; | |
//split onto multiple lines for readability | |
while((std::cout << "Please enter a number: ") //make sure the user knows what you expect | |
&& std::getline(std::cin, line) | |
) //this loop runs until we break out of it | |
{ | |
std::istringstream is {line}; | |
if((is >> num) && !(is >> line)) //re-using `line` to test for extra stuff after the number | |
{ | |
break; //done, we got what we wanted | |
} | |
std::cerr << "Invalid input, try again." << std::endl; | |
} | |
} |
Now, input like "123abc" will be rejected too, because the "abc" will be successfully read into a string. Great! Except, well, this will also reject input like "123 456" because there are two tokens there. In most cases, that's good - you want that to happen. But what if, say, you wanted to let the user enter a bunch of numbers on the same line? Like "123 456abc 789"? You're already set to go! Just remember that you should consider the entire line invalid if one of the parts is wrong, and thus have the user re-enter the entire line. This is fine - most consoles let you simply press the up arrow to retype the previous input, and then use the left and right arrow keys to edit.
You could use a split function you wrote for std::vector
, or you could use a neat but inefficient trick since user input is typically not very long. Either way, you now have an excellent way to process user input. Hooray!
That's easy enough to do for both of the above examples - below are the corresponding changes you would need to make (pay attention to the line numbers):
&& !(std::istringstream{line} >> num) | |
&& (num < min || num > max) | |
) |
if((is >> num) && !(is >> line) | |
&& (num > min && num < max)) | |
{ |
max
and min
can be constants, variables, or literal numbers if you replace them. Don't forget, you should modify the prompt so the user knows about the range restriction.