02 - states

Stream states

Each C++ stream has a state that holds information about special events (mainly errors). Error handling itself is complex enough that it's covered in separate chapter. For now, you should remember that after any error, all subsequent I/O on the same stream will fail immediately untill the error state is explicitly cleared.

Streams have defined convertion to bool so they can be put into conditional statements like if and while to check for any non-ideal state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while (std::cin >> value)
{
	// use value...
}

// equivalent code
while (true)
{
	std::cin >> value;

	if (std::cin)
	{
		// use value...
	}
	else
	{
		break;
	}
}

Each C++ stream has a state that informs about potential errors. The state is stored on a specific bitmask type called std::ios_base::iostate.

There are 3 possible flags (values implementation-defined):

  • std::ios_base::badbit - represents unrecoverable error (a crucial operation such as memory allocation failed).

  • std::ios_base::failbit - represents recoverable error (ususally a retryable operation such as formatted I/O failed).

  • std::ios_base::eofbit - set when all data has been processed and there is no possibility of continuing (e.g. end of file, closed socket). If reading interactively (e.g. std::cin which was not redirected to read from a file), the EOF can be sent to the program through the console:

    • Unix console terminals: ctrl+D (though does not work in Git Bash on Windows)

    • Windows cmd: ctrl+Z

    • Windows Power Shell: ctrl+Z

There is also std::ios_base::goodbit which has value 0 - if comparison of stream's state is equal to goodbit, it means no error bit flags are set.

The table below showcases how different functions behave for different states:

TODO table from https://en.cppreference.com/w/cpp/io/ios_base/iostate TODO this table needs CSS support for background and reST support for column span.

If any of error flags is set:

  • it will stay permanently until explicitly cleared

  • all I/O on this stream will fail immediately

Correct reaction to different flags is crucial in implementing robust I/O. Generally:

  • if bad bit is encountered, the entire I/O should be redone as stream's state is not recoverable

  • if fail bit is encountered:

    • if reading interactively: it should be cleared and last I/O operation should be retried

    • if reading from premade input (e.g. file) - stop processing, similarly to bad bit

  • if EOF (end of file) is encountered, this means no more data will be given and any I/O-handling code should proceed to processing of the data

Simple handling

The following program reads numbers untill anything wrong happens.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main()
{
	double sum = 0;
	int number_count = 0;

	double d;
	while (std::cin >> d) // uses convertion to bool
	{
		// if code gets here it means last I/O operation succeeded
		sum += d;
		++number_count;
	}

	std::cout << "numbers entered: " << number_count << "\n";
	// division by 0 is only undefined behavior on integers
	// but in this case floating-point division by 0 makes no sense
	if (number_count != 0)
		std::cout << "average of entered numbers: " << sum / number_count << "\n";
}

Notes:

  • This program doesn't differentiate between error conditions (recoverable, unrecoverable, EOF). This approach is only useful if you want to stop upon any problem.

  • EOF alone does not make convertion to bool evaluate to false, but any operation after reaching EOF will fail and set failbit so eventually the loop will stop. This behavior is desirable because last characters in the input can result in both successful read and setting EOF - in such case we still want to accept last chunk of data and stop in the next iteration.

Advanced error handling

The following program reacts differently to different problems:

  • failed I/O - operation is tried again

  • EOF - reading stops

  • badbit - reading stops, warning is printed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <limits>

int main()
{
	double sum = 0;
	int number_count = 0;

	while (true)
	{
		double d;
		if (std::cin >> d)
		{
			// if code gets here it means I/O operation succeeded
			sum += d;
			++number_count;
		}

		if (std::cin.bad())
		{
			std::cout << "unrecoverable error\n";
			break;
		}

		// it's quite common to have both eof and fail bits set together
		// in such case it's better to handle eof first and just stop I/O
		if (std::cin.eof())
		{
			std::cout << "end of input, proceeding with calculations\n";
			break;
		}

		if (std::cin.fail())
		{
			std::cout << "input failed, try again\n";
			std::cin.clear();
			// ignore all characters in the stream or until line break is encountered
			// without this line, the same invalid input would be tried endlessly
			std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
		}
	}

	std::cout << "numbers entered: " << number_count << "\n";
	if (number_count != 0)
		std::cout << "average of entered numbers: " << sum / number_count << "\n";
}

This program is much more detailed and while it deals with different situations appropriately, I wouldn't call it's code to be easy to write - there are lots of ways to make potential bugs:

  • different order of conditions inside the loop - faulty logic or at least unclear error messages

  • missing clear - the loop would endlessly stay in fail state

  • missing ignore - the loop would endlessly process fautly data

  • missing breaks - faulty logic or endless loop

For this reason C++ streams get a lot of criticism - they support a lot of customization but at the same time doing even simple I/O operations is complicated and contains many opportunities to commit a bug that results in an endless loop or invalid data. Additionally each formatted I/O operation can treat whitespace differently. My recommendation is thus:

  • For non-interactive input (e.g. text from a file), use the simple approach of testing convertion to bool (basically first example).

  • For interactive input use line-oriented functions (explained in a later lesson) and do any input validation yourself - this will detatch your code from dealing with stream states, allow custom (potentially very complex) validation and make retrying operations easier.