Chapter 17
Validation: Assuming the Worst
Checking input, at versus square brackets, and optional.
Chapter 5 showed you that typing letters into an int leaves it at zero and jams
cin forever. Chapter 8 showed you what that does inside a loop, gave you an
idiom that behaves, and said the explanation waits for chapter 17. Chapter 10 told
you nothing checks the number in your square brackets, and said the same thing.
This is chapter 17.
What a stream is doing in an if
while (std::cin >> x) {
Chapter 8 called this a shape to copy. Here’s what’s under it.
std::cin >> x does the read and then hands back std::cin itself. That’s why
you can chain them. std::cin >> a >> b is the second >> acting on what the
first gave back. And a stream used where a bool is wanted answers a single
question: has everything gone fine so far?
So the loop means: read, and keep going as long as the reads are working. Two things stop it: running out of input, and input that doesn’t fit. Both leave the stream in a failed state, and a failed stream is false.
That is also the whole explanation for chapter 15’s file loop. std::getline(in, line) returns the stream, the stream goes false at the end of the file, the loop
ends. One idea, two places you’d already met it.
Un-jamming a stream
A failed stream stays failed. Every later read does nothing at all, which is why chapter 5’s program went quiet and chapter 8’s ran away.
Two calls fix it:
#include <limits>
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
clear() resets the failed flag, so the stream will attempt reads again. On its own
that’s not enough, because the offending text is still sitting there waiting to
be read and fail again. ignore throws characters away: up to that
mouthful-of-a-number’s worth, or until it hits a newline, whichever comes first.
In practice it means “discard the rest of this line”.
That number is genuinely how you write “as many as it takes”. It’s ugly, it needs
<limits>, and you copy it.
Put together, the pattern for demanding a number until you get one:
int n = 0;
std::cout << "A number: ";
while (!(std::cin >> n)) {
std::cout << "That was not a number. Try again: ";
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
A number: banana
That was not a number. Try again: hello
That was not a number. Try again: 42
got 42
Read the condition as “while the read did not succeed”. Chapter 5’s trap, closed and this is worth wrapping in a function, because you’ll want it everywhere:
int read_int(const std::string& prompt);
at, and the brackets that check
Chapter 10 admitted that v[7] on a three-element vector does something undefined
and moved on. Here’s the alternative:
std::vector<int> v{1, 2, 3};
std::cout << v.at(1) << '\n'; // 2
std::cout << v.at(7) << '\n'; // stops the program
at(1) = 2
about to call at(7)
libc++abi: terminating due to uncaught exception of type
std::out_of_range: vector
at does the same job as [] and checks first. Out of range, and instead of
quietly reading rubbish, your program stops and says so.
The wording above is clang’s; GNU’s says much the same thing with terminate called after throwing an instance of 'std::out_of_range' and usually tells you
both numbers. Either way the useful words are out_of_range, and they name your
mistake exactly.
So should you use at everywhere? No. Use [] when the index obviously can’t
be wrong, such as a counting loop up to size(), which is most of them. Use at when
the number came from outside your program: a reader typed it, a file supplied it,
some arithmetic produced it. The rule is about where the number came from, not
about being careful.
When a function has no answer
Chapter 14’s stretch left you with a genuine problem. find_contact returns a
Contact, so what does it return when there isn’t one?
Every option is bad. An empty contact is a lie the caller has to remember to check
for. A bool return with the contact through a reference parameter works and
reads horribly. Crashing is too much.
#include <optional>
std::optional<Contact> find_contact(const std::vector<Contact>& book,
const std::string& name) {
for (const Contact& c : book) {
if (c.name == name) {
return c;
}
}
return std::nullopt;
}
std::optional<Contact> means “a Contact, or nothing at all”, and it says so in
the type where the caller cannot miss it. std::nullopt is how you return the
nothing.
Using one is an if:
std::optional<Contact> found = find_contact(book, "Ada");
if (found) {
std::cout << "found " << found.value().name << '\n';
} else {
std::cout << "no such contact\n";
}
found Ada
no such contact
The optional itself is true when it holds something. .value() gets at what’s
inside. You’ll also see found->name, which is shorthand for the same thing and
uses punctuation chapter 23 explains.
There’s a shortcut for “or use this instead”:
find_contact(book, "Nobody").value_or(Contact{"(none)", "-"})
And a way to get it wrong, which behaves exactly like at:
libc++abi: terminating due to uncaught exception of type
std::bad_optional_access: bad_optional_access
That’s .value() on an empty optional. Same shape of crash, same kind of message,
and the type name tells you precisely what you did. Check before you unwrap.
Where to check
Checking everything everywhere makes a program unreadable, and checking nothing makes it fragile. The useful idea is a line drawn around your program.
Outside the line is anything you didn’t produce: what the reader types, what a file contains, what a name lookup returned, and anything computed from those. Inside the line is data your own code has already checked.
Check at the crossing, once, and then trust it. A read_int that cannot return
nonsense means every function it feeds can take a plain int and stop worrying.
The alternative, every function defensively re-checking its arguments, is twice
the code and still misses cases, because a function that finds a bad value halfway
through a program has no idea what to do about it. The place that read it does.
This is why the checks in this chapter cluster in two spots: where input arrives, and where a lookup might fail. Everywhere else, the types do the work.
Exercise 1 · Refuse to be broken
Write int read_int(const std::string& prompt) using the clear/ignore
pattern. Call it three times and try every awful thing you can think of: letters,
an empty line, 12abc, a very long string of junk.
Then write std::optional<int> to_int(const std::string& text) that returns
nothing when the text isn’t a number. Reading a std::string into an
std::istringstream and checking whether it worked is one way; searching for a
non-digit is another.
Finally, take a vector of five numbers, ask the reader for an index, and print
it with at. Type 99 and read the crash properly rather than flinching from it.
Check yourself
Project
The unbreakable to-do list
Roughly 60 minutes
No new program. Take chapter 15’s to-do list and spend an hour trying to destroy it, fixing what you find.
Start by breaking it. Run it and type, in this order: banana at the menu,
an empty line, 0 for a removal, 99 for a removal, -1, then hold a key down
and mash the return key. Write down what each one does. At least two of them will
be worse than you expect, and the menu one will hang the program.
Then fix it, in this order:
- The menu. Replace
std::cin >> choicewith aread_intthat refuses to return until it has a number. This alone fixes the hang. - The range. A menu choice of 7, or a removal number of 99, should say so
and carry on. This is an
if, not anat. You don’t want a crash here, you want a polite refusal. - The file. Check that the
ifstreamopened. A missing file on first run is fine and should produce an empty list; a file you can’t read is different, and worth telling the reader about rather than silently starting empty. - The empty item. Adding a blank to-do is allowed by the code and useless to the reader. Refuse it.
What you are doing has a name. Every one of these fixes is the same move: find the boundary between your program and the outside world, and stop trusting it. Inside that line you can assume things; outside it you can assume nothing. Reader input, file contents, and anything derived from them are outside.
Stretch one, find honestly. Add a search that returns
std::optional<std::string> for the first item containing some text. Print the
match or “nothing matched”, with no sentinel values and no lying.
Stretch two, hand it over. Give the program to somebody else and watch them use it without saying anything. This is uncomfortable and worth more than any amount of testing it yourself, because you have been carefully typing sensible input for an hour without noticing.