C++: Console User Input Done Right

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:

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!

What if I only want certain numbers, e.g. numbers within a range?

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.