Parsing a text file
A text file is a sequence of lines, and a line is a sequence of fields. Parsing is the loop that reads a line, splits it, converts the fields and stores the record, with a decision for every line that does not fit.
The pieces are already known: getline and string streams from lesson 065, the file itself from lesson 067, and conversions from lesson 017. This lesson puts them together and adds the part that matters in practice: what to do with a bad line.
Splitting a line
The program splits one CSV line into fields with std::getline and a comma delimiter. It prints each field in brackets and the count, which shows the empty field between two commas.
A split function that returns a vector of strings is the reusable form; here the loop is inline so the page stays short. The loop ends when getline returns a stream that is false.
Empty fields are part of the format, and a trailing comma adds one more. Code that assumes every field has content has to check the size of the result before indexing.
#include <iostream>
#include <sstream>
#include <string>
int main()
{
std::istringstream line{"ada,36,mathematician"};
std::string field;
int count = 0;
while (std::getline(line, field, ',')) {
std::cout << '[' << field << ']';
++count;
}
std::cout << ' ' << count << '\n';
}
g++ -std=c++23 -Wall -Wextra split.cpp -o split_csv
./split_csv
[ada][36][mathematician] 3
A file of records
The file has one record per line: a name, a comma, a number. The loop reads a line, splits it, converts the second field and stores the pair, and the sum is printed at the end.
The program writes the file first so the example is self-contained. The data is the same shape as a CSV export, and the code is the loop that would read one.
The conversion is std::stoi, which stops at the first character that is not a digit and does not report how much it consumed. The fourth section checks the whole field instead.
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main()
{
std::ofstream{"people.csv"} << "ada,3\nbob,5\ncyd,2\n";
std::ifstream in{"people.csv"};
std::string line;
std::vector<std::pair<std::string, int>> people;
while (std::getline(in, line)) {
std::istringstream fields{line};
std::string name;
std::string number;
std::getline(fields, name, ',');
std::getline(fields, number, ',');
people.emplace_back(name, std::stoi(number));
}
int total = 0;
for (const auto& [name, count] : people) {
total += count;
}
std::cout << people.size() << ' ' << total << '\n';
}
g++ -std=c++23 -Wall -Wextra read.cpp -o read_csv
./read_csv
3 10
Reporting the bad line
A file from outside the program will eventually contain a line that does not parse. The program below counts the line numbers, converts each value and reports the first line whose number is not a number, with its number.
The file has a valid line, an empty line and another valid line. The empty line is the failure, and the report says line 2, which is what a user needs to fix the file.
The program keeps going after the bad line, which is the right behaviour for a report: one broken row should not hide the rest. A program that must be all-or-nothing stops at the first error instead.
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
int main()
{
std::ofstream{"broken.csv"} << "ada,3\n\nbob,5\n";
std::ifstream in{"broken.csv"};
std::string line;
int number = 0;
int good = 0;
while (std::getline(in, line)) {
++number;
if (line.empty()) {
std::cout << "line " << number << ": empty\n";
continue;
}
try {
std::stoi(line.substr(line.find(',') + 1));
++good;
} catch (const std::invalid_argument&) {
std::cout << "line " << number << ": not a number\n";
}
}
std::cout << good << " good\n";
}
g++ -std=c++23 -Wall -Wextra report.cpp -o report
./report
line 2: empty
2 good
Checking the whole field
std::stoi has an optional second argument: a pointer to a std::size_t that receives the number of characters consumed. If that count is not the length of the field, the field had trailing text.
The program parses "12abc" and prints the value and the position where parsing stopped. The value is 12 and the position is 2, which is how a program rejects the field instead of silently using half of it.
This check is the difference between a parser and a converter. It costs one comparison and it catches the file that contains "12 kg" where the format promised a number.
#include <iostream>
#include <string>
int main()
{
std::string field = "12abc";
std::size_t used = 0;
int value = std::stoi(field, &used);
std::cout << value << ' ' << used << ' ' << (used == field.size()) << '\n';
}
g++ -std=c++23 -Wall -Wextra checked.cpp -o checked
./checked
12 2 0
A pointer of the wrong type
The second argument of std::stoi is a std::size_t*, not an int*. The compiler cannot convert one pointer type into the other, and it says so at the call, which is the mistake most programs make once when they add the check.
The program passes the address of an int and the compiler reports an invalid conversion from int* to size_t*. The fix is a std::size_t variable, which is also the type the standard library uses for sizes everywhere.
The runtime counterpart of this mistake is an empty field: std::stoi on a string with no digits throws std::invalid_argument, which the report section catches. Checking the field before converting is still the cheaper habit.
#include <iostream>
#include <string>
int main()
{
std::string field = "12abc";
int used = 0;
int value = std::stoi(field, &used);
std::cout << value << '\n';
}
g++ -std=c++23 -Wall -Wextra wrong_pointer.cpp -o wrong_pointer
wrong_pointer.cpp: In function 'int main()':
wrong_pointer.cpp:9:26: error: no matching function for call to 'stoi(std::string&, int*)'
9 | int value = std::stoi(field, &used);
| ~~~~~~~~~^~~~~~~~~~~~~~
wrong_pointer.cpp:9:26: note: there are 2 candidates
In file included from /usr/include/c++/15.2.0/string:56,
from /usr/include/c++/15.2.0/bits/locale_classes.h:42,
from /usr/include/c++/15.2.0/bits/ios_base.h:43,
from /usr/include/c++/15.2.0/ios:46,
from /usr/include/c++/15.2.0/bits/ostream.h:43,
from /usr/include/c++/15.2.0/ostream:42,
from /usr/include/c++/15.2.0/iostream:43,
from wrong_pointer.cpp:1:
/usr/include/c++/15.2.0/bits/basic_string.h:4454:3: note: candidate 1: 'int std::__cxx11::stoi(const std::string&, std::size_t*, int)'
4454 | stoi(const string& __str, size_t* __idx = 0, int __base = 10)
| ^~~~
/usr/include/c++/15.2.0/bits/basic_string.h:4454:37: note: no known conversion for argument 2 from 'int*' to 'std::size_t*' {aka 'long unsigned int*'}
4454 | stoi(const string& __str, size_t* __idx = 0, int __base = 10)
| ~~~~~~~~^~~~~~~~~
/usr/include/c++/15.2.0/bits/basic_string.h:4725:3: note: candidate 2: 'int std::__cxx11::stoi(const std::wstring&, std::size_t*, int)'
4725 | stoi(const wstring& __str, size_t* __idx = 0, int __base = 10)
| ^~~~
/usr/include/c++/15.2.0/bits/basic_string.h:4725:23: note: no known conversion for argument 1 from 'std::string' {aka 'std::__cxx11::basic_string<char>'} to 'const std::wstring&' {aka 'const std::__cxx11::basic_string<wchar_t>&'}
4725 | stoi(const wstring& __str, size_t* __idx = 0, int __base = 10)
| ~~~~~~~~~~~~~~~^~~~~
Exercises
- Read a file of three columns and print the row with the largest value in the second column.
- Skip empty lines and comment lines that start with #, and count the records you kept.
- Write a parser that accepts "12 kg" as a number and a unit, and rejects "12kg" without a space.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is the shape of a parsing loop?
Answer
Read a line, split it, convert the fields, store the record
Each line becomes one record, and the bad lines are the decision in the middle.
-
What is the usual separator for a CSV line?
Answer
A comma
The name says comma-separated values, and the lesson splits on it.
-
How do you convert a field to int?
Answer
std::stoi, which throws on bad text
stoi parses the digits and throws std::invalid_argument for the rest.
-
What should the parser do with a line that has too few fields?
Answer
Report the line and skip it
The report names the line, and the other records survive.
-
What does this program print?
#include <iostream> #include <sstream> #include <string> #include <vector> int main() { std::string line = "ada,36,mathematician"; std::istringstream input{line}; std::vector<std::string> fields; std::string field; while (std::getline(input, field, ',')) fields.push_back(field); for (const auto& f : fields) std::cout << '[' << f << ']'; std::cout << ' ' << fields.size(); }Answer
[ada][36][mathematician] 3
The delimiter split gives three fields, and the count is three.
-
What does this program print?
#include <fstream> #include <iostream> #include <sstream> #include <string> int main() { { std::ofstream out{"people.csv"}; out << "ada,20\nbob,30\ncyd,10\n"; } std::ifstream in{"people.csv"}; std::string line; int count = 0, total = 0; while (std::getline(in, line)) { std::istringstream row{line}; std::string name; int age = 0; std::getline(row, name, ','); row >> age; ++count; total += age; } std::cout << count << ' ' << total / count; }Answer
3 20
Three records are read, and their average age is 20.
-
What does this program print?
#include <fstream> #include <iostream> #include <sstream> #include <string> int main() { { std::ofstream out{"report.csv"}; out << "ada,36\n\nbob,41\n"; } std::ifstream in{"report.csv"}; std::string line; int line_number = 0, good = 0; while (std::getline(in, line)) { ++line_number; if (line.empty()) { std::cout << "line " << line_number << ": empty\n"; continue; } ++good; } std::cout << good << " good"; }Answer
line 2: empty / 2 good
The empty second line is reported, and the two records are counted.
-
What does this program print?
#include <iostream> #include <sstream> #include <string> int main() { std::string field = "12abc"; std::size_t used = 0; int value = std::stoi(field, &used); std::cout << value << ' ' << used << ' ' << (used == field.size()); }Answer
12 2 0
stoi reads the digits and stops at a, so two characters were used and the whole field was not consumed.
-
What does the compiler report for this line?
#include <string> int main() { std::string field = "12"; int used = 0; int value = std::stoi(field, &used); return value + used; }Answer
no matching function for call to 'stoi(std::string&, int*)'
The second parameter is a size_t pointer, and an int* does not convert to it.
-
Why check the whole field after stoi?
Answer
stoi accepts a prefix and ignores the rest
The used count or a stream extraction tells whether the parse consumed everything.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev