Video Coming Soon...
19: Input From a User
I'm going to warn you that C++'s Input/Output (I/O) system is absolutely terrible. You should approach this exercise with a critical eye, but understand that you'll have to use this style of API in C++ so understanding it helps later. We'll discuss the issue with this system, how to use it, and then learn to use it with files in the next exercise.
Input/Output the Hard Way
C++ uses a system to get input from a user that's quite odd in the world of programming. Most languages use some form of functions to process data. For example, in Python I might do this:
with open("stuff.txt") as f:
data = f.read()
C++ though uses a mixture of functions and operators to do input, and uses them in a way that's typically ambiguous when you look at the code. If you look at the python above, it'll read the data. Done.
If you look at this C++ code can you tell what's being read?
cin >> data;
Well? Is it a string? A number? How big is the string if it's a string? Can you read more? How much data did you read? Compared to the Python code you can't figure out really what's going on and have to refer to other code to figure out what this one is doing.
This gets worse because mixed into these operators is also usage of functions to modify the input, sometimes on the input and sometimes between operators:
while(!cin) {
cin.clear();
cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
println("Wrong, enter a number:");
cin >> age;
}
This monstrosity is how you ask a user for a number until they get it right. Yes, you have to pull in this std::numeric_limits<std::streamsize>::max()
to clear the input so you can ask again, and pass that to .ignore()
, oh and also .clear()
, but clear()
doesn't clear, that's what .ignore()
does, and then after that cin >> age
? What? I thought we were calling functions so now we switch back to >>
?
This is one reason why I have you use fmt::println()
to do output because that system is far better. It's so good even the C++ committee adopted it for C++20. They just haven't taken the next logical step of getting rid of the input API in the same way.
If It Sucks Why Learn It?
You'll find that I spend time telling you my opinion on something, but still have you learn it. This is for two very important reasons:
- No matter how bad this is, you still have to deal with it. By telling you that I think it's not very good I'm hoping you won't blame yourself if you struggle with it. It's not you, it's them.
- The way you get better at programming and design is to look at other people's code (and your own) with a critical eye. All systems are created by people, and people have bad ideas like
std::numeric_limits<std::streamsize>::max()
. Just because that person is the creator of C++ doesn't mean his code is an unassailable truth that can't be questioned. Question everything, or you'll forever be using garbage.
The Code
I want you to do this one slowly and to think about how you might improve this given what you know. Get it to work first, but we'll soon try to "fix" it.
View Source file ex19.cpp Only
#include <iostream>
#include <fmt/core.h>
#include <limits>
using std::cin, std::cout, std::getline, std::string;
using namespace fmt;
int main() {
std::streamsize eatme = std::numeric_limits<std::streamsize>::max();
string name;
println("What's your name?");
cin >> name;
int age;
println("What's your age?");
cin >> age;
while(!cin) {
cin.clear();
cin.ignore(eatme, '\n');
println("Wrong, enter a number:");
cin >> age;
}
// eat the newline
cin.ignore(eatme, '\n');
string quote;
println("Tell me something about you?");
getline(cin, quote);
println("---\nHello {}, you are {} years old.",
name, age);
println("Something you about you:");
cout << quote;
}
Code Walkthrough
Before you continue with my walkthrough I want you to study the std::cin documentation and add comments to this code explaining it. You'll most likely have to study multiple functions on cppreference.com to fully understand it, but this is something you must do with so much code. I think a major part of my programming work is simply studying documentation and code to understand something (then writing my own version once I do).
Once you do see if my descriptions of the key parts of the code match yours:
std::streamsize eatme = std::numeric_limits<std::streamsize>::max();
- This irritating line of code is used later to get the "max buffer" size. You use this to tell
cin
to clear its buffer by "eating everything until max buffer size." cin.ignore(eatme, '\n')
- This uses the
eatme
variable to tellcin
to do this eating of characters (for max buffer size), until it reaches the'\n'
character then stop. Readignore()
documentation. cin.clear()
- But wait, you did
.clear()
too, doesn't that clear the buffer? No, this just clears the error condition on the buffer. You then have to clear...uh I mean...ignore any characters in the buffer because it's cleared but not cleared. Yes, it is truly that dumb. Readclear()
documentation.. cin >> age
- You use the
>>
operator to "extract" types of data from the input stream. The stream will detect the type you gave and then "automagically" require that format in the input.
Most everything else in this code is familiar to you, and if not then be sure to go back and study previous exercises where you may have missed something.
Practicing Functions
A lot of this code could go in functions that make it easier, so your challenge is to clean up this code with functions. After you've cleaned it up, try to use your new version to ask more questions of the user.
For example, you know that gnarly way of clearing the input? You could make a function like this:
void clear_input() {
std::streamsize eatme = std::numeric_limits<std::streamsize>::max();
cin.clear();
cin.ignore(eatme, '\n');
}
Now when you need to clear the input you call this function, like this:
while(!cin) {
clear_input();
println("Wrong, enter a number:");
cin >> age;
}
Now you could probably turn this code into a function too. Just keep turning pieces of code into functions until everything is nicer to work with, then use your new code to ask users more questions.
Break It
Breaking this code involves mostly throwing garbage at it. Some examples are:
- Find a file with unrelated data, like a Word document,
.csv
file, or a.zip
file. - You can "pipe" this file to your program with
cat garbage.zip | ex19
and watch it burn...maybe. - Your screen might get wrecked so be ready to close your terminal and open a new one if it's messed up.
- On Unix systems you should have a
/dev/random
or/dev/urandom
file that you can use instead. - ADVANCED Write your own "fuzzer" which is a program that prints smarter garbage that you then feed into this program to break it. You'd run it the same way with
fuzzer | ex19
to feed it.
A "fuzzer" is a special testing program that purposefully feeds garbage and unexpected input to a system. It's a useful testing technique for detecting unexpected input situations you need to handle, and there's some advanced fuzzing systems out there that can do very complex fuzzing.
Further Study
- Read through the std::istream and std::ostream documentation.
- ADVANCED Look into fuzzing tools like Google FuzzTest and AFL. See if you can get one to work.
Register for Learn C++ the Hard Way
Register to gain access to additional videos which demonstrate each exercise. Videos are priced to cover the cost of hosting.