Foundations · Module 4 · Lesson 6 of 9
Reading Text Files and Handling Errors With ifstream
Read a file line by line with std::ifstream, tell a normal end of file from a real error using the stream state, and handle empty files, missing newlines, Windows line endings and malformed lines.
By the end you can
- Open a file for reading, check it, and read it line by line with the read as the loop condition
- Distinguish a normal end of file from an I/O error using the stream state
- Predict how getline treats a last line without a newline
- Handle line-ending and format problems in files that come from other systems
Builds on Writing Text Files With ofstream, Reading Input and Surviving Bad Input.
01 · A question first
A file is input from the outside world, as untrustworthy as the keyboard: it may be missing, empty, cut off, or written on another system. How does a read loop know the difference between the end of the file and a failing disk? What happens to a last line with no newline? And why is the loop that tests eof first the classic bug?
Make a guess, then step through the trace below to check it.
02 · Watch it run
std::ifstream in("notes.txt");std::string line;int count = 0;while (std::getline(in, line)) { count = count + 1;}bool ok = !in.bad();- part 0pos: 0
- part 1line: --
- part 2state: good(looked at in this step)
- part 3count: 0
The file notes.txt holds Ada,36, then a newline, then Lin,28 with no newline after it: 13 bytes. Opening for reading does not create or change the file, and it fails if the file does not exist. The four cells are the read position in the file, the last line read, the stream state, and a counter of lines.
Machine work: open the file for reading
03 · The idea
Start with the problem
The file you wrote in the last lesson is only useful if you can read it back, and files also come from other people and other programs: configuration, data to import, logs to analyze. Treat a file as input from the outside world, as untrustworthy as the keyboard of lesson 5 in module 1. It may be missing, empty, cut off in the middle, edited by hand, or written on another operating system.
The basic loop is short:
std::ifstream in("contacts.txt");
std::string line;
while (std::getline(in, line)) {
// use line
}What is hidden in those lines is the interesting part: how the loop ends, what a failure looks like, and which odd files break it.
How the idea works
Opening. An std::ifstream, from the header <fstream>, opens a file for reading. It never creates or changes the file, and it fails if the file does not exist or cannot be read. Check it right away with if (!in).
Reading lines. The call std::getline(in, line) reads characters up to the next newline, discards the newline, and stores the rest in line, replacing whatever the string held before. It returns the stream, and a stream in a condition is true when the last operation succeeded. Making the read the loop condition is the same idiom as while (std::cin >> x) from module 1.
A complete function. Using the output-parameter pattern from lesson 3, here is a function that reads all the lines of a file:
bool readLines(const std::string& path, std::vector<std::string>& lines) {
std::ifstream in(path);
if (!in) {
return false;
}
std::string line;
while (std::getline(in, line)) {
lines.push_back(line);
}
return !in.bad();
}How the loop ends. The trace shows the three possibilities. A getline that extracts at least one character succeeds, even when the end of the file arrives before a newline: the last line is not lost (step 3). A getline that finds nothing sets the failbit, together with the eofbit for running out of data, and the loop ends (step 4). A genuine I/O failure sets the badbit instead. So after the loop, !in.bad() tells you that the loop ended because the file was finished, and not because something broke (step 5).
Records and positions. Lines, once read, are ordinary strings, and the next lesson but one shows how to split them into fields. You can also keep a line counter and report problems in the format file:line: message, which is the shape of the compiler diagnostics from lesson 6 of module 1 and tells the user exactly where to look. Formatted extraction with >> also works on a file stream, with the same failure rules as std::cin.
Seeking. A file stream can go back: in.clear() resets the flags, and in.seekg(0) moves to the start so that the file can be read again. The call in.tellg() reports the current position.
Why it is correct
Putting the read in the loop condition ties the loop to success, so the body never sees a line that was not read. The behavior of getline at the end follows from one rule: it fails only when it extracts nothing. That is why a last line without a newline is delivered, and an empty file produces no iterations at all.
The three flags answer three different questions. The eofbit says that the input ended, the failbit says that the last operation did not produce a value, and the badbit says that the stream itself is broken. Together they let the program choose between finishing normally and reporting an error. Reading in blocks behind the scenes makes all of this fast, because lines come from memory.
What it costs
Reading speed. An ifstream reads the file in blocks of a few kilobytes with a read system call each, and getline copies from that buffer into the string. A typical rate is a few hundred megabytes a second, so reading a 1 GB text file line by line takes a few seconds. The first read comes from the disk, and later reads are served from the operating system's cache.
Allocation. Declaring line outside the loop lets its buffer be reused, as the growth rules of lesson 4 in module 3 allow, so most iterations allocate nothing.
Memory. Keeping every line in a vector takes the size of the file plus the overhead of each string, which is 32 bytes for the object and a heap allocation for lines longer than the small-string buffer. A file of a million 20-character lines, about 21 MB, can take around three times that much memory.
Checks. Testing the flags costs nothing.
Pitfalls and variations
The eof loop. while (!in.eof()) tests the flag before the read. The flag is set only after a read has hit the end, so the loop runs once more and handles an empty or stale line. Use the read as the condition.
Unchecked open. A stream that failed to open produces no lines, and the program quietly behaves as if the file were empty.
Confusing end with failure. After the loop, a program that does not test bad() cannot say whether it read the whole file.
Windows line endings. A file with CRLF line endings leaves a carriage return at the end of each line on Linux. Remove it with a test such as if (!line.empty() && line.back() == '\r') line.pop_back();.
A byte order mark. Some Windows tools put three bytes at the start of a UTF-8 file. They appear at the start of the first line and break comparisons.
Records that span lines. A CSV field in quotes may contain a newline, which a line-by-line reader splits in two.
Huge lines. getline accepts any length, so a hostile file with a single 10 GB line exhausts memory. For untrusted files, read in pieces with a limit.
Check then open. Testing std::filesystem::exists before opening is a race, because the file may appear or vanish in between. Try to open it and check the result.
Variations. std::getline(in, line, ',') uses another delimiter. An std::istringstream parses a line that is already in memory. std::filesystem::file_size gives the size of a file before you read it, and memory-mapped files, through operating system calls, avoid the copying altogether.
Where it is used
Configuration loaders, importers for CSV and log files, test programs that read their input data from files, and tools that process other tools' output all start with this loop. Writing it with the error handling of this lesson is what turns a quick script into a tool that you can trust with other people's files.
The next lesson, "Binary Files: Writing and Reading Raw Bytes", leaves text behind and works with the bytes themselves, with the layout traps of lesson 2 in mind.
04 · Prove it
Question 1 of 4 · answer from memory
A file contains `Ada,36` and `Lin,28` on two lines, with no newline after the last line. How many times does the body of `while (std::getline(in, line))` run?
05 · Practice
Asked in interviews
Something wrong with this lesson? Report a problem