Contents

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

1. Why does while (std::cin >> x) end when the reader types letters?

Not quite. Close, but it returns the stream itself. That the stream then acts as false is a separate step, and the one that lets you chain reads.

Yes. The same mechanism ends a getline loop at the end of a file. One idea covering both cases.

Not quite. Streams do not throw by default. They set a failure flag and go quiet, which is precisely why a jammed cin is so confusing.

2. Why is clear() alone not enough to recover from bad input?

Yes. Without ignore, the very next read hits the same characters and fails again, which looks like clear() did nothing.

Not quite. It works on any stream. The problem is what it does not do, which is discard anything.

Not quite. There is nothing to reopen. clear plus ignore is the whole recovery.

3. When should you use at instead of square brackets?

Not quite. It costs a check every access, and in a counting loop up to size() the check can never fire. Use it where an index could actually be wrong.

Yes. Reader input, file contents, or arithmetic you are unsure of. The question is where the number came from, not how careful you feel.

Not quite. Backwards. Vectors have at; on a map, plain brackets create missing entries rather than failing, as chapter 16 showed.

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:

  1. The menu. Replace std::cin >> choice with a read_int that refuses to return until it has a number. This alone fixes the hang.
  2. The range. A menu choice of 7, or a removal number of 99, should say so and carry on. This is an if, not an at. You don’t want a crash here, you want a polite refusal.
  3. The file. Check that the ifstream opened. 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.
  4. 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.