Parsing structured text in C++ comes down to splitting a line into fields and then converting each field to the type you want. The standard library gives you the building blocks — [[std::getline]] for lines, [[std::stringstream]] for whitespace, [[std::from_chars]] for numbers — and the rest is careful handling of the separators, the quoting and the trailing characters.
Splitting on a delimiter
The basic operation is splitting a string on a separator. std::string::find gives you the position, and std::string::substr takes the piece.
#include
#include
#include
std::vector split(const std::string& text, char delimiter) {
std::vector fields;
std::string::size_type start = 0;
while (true) {
std::string::size_type position = text.find(delimiter, start);
if (position == std::string::npos) {
fields.push_back(text.substr(start));
break;
}
fields.push_back(text.substr(start, position - start));
start = position + 1;
}
return fields;
}
int main() {
std::string line = "m-001|Alice|2024";
for (const std::string& field : split(line, '|')) {
std::cout << "[" << field << "]\n";
}
return 0;
}The output shows the line split into its three fields:
[m-001]
[Alice]
[2024]The loop is written with an explicit break rather than a condition on find, because the last field has no delimiter after it and must still be pushed.
Reading whitespace-separated data
When the fields are separated by runs of whitespace rather than a single character, a string stream does the work for you.
#include
#include
#include
int main() {
std::string line = " 12 34.5 frost ";
std::istringstream stream(line);
int count = 0;
double weight = 0.0;
std::string condition = "unknown";
if (stream >> count >> weight >> condition) {
std::cout << "count: " << count << '\n';
std::cout << "weight: " << weight << '\n';
std::cout << "condition: " << condition << '\n';
} else {
std::cout << "could not parse the line\n";
}
return 0;
}The output shows the three fields extracted, with the leading and trailing whitespace skipped automatically:
count: 12
weight: 34.5
condition: frostThe trick is checking the extraction. If the text is not a number where a number is expected, the extraction fails and the variables keep their old values.
Handling quoted CSV fields
A comma-separated value file may quote a field that contains the separator, and the quoted field may itself contain an escaped quote. A plain split on the comma gets this wrong.
#include
#include
#include
std::vector parse_csv_line(const std::string& line) {
std::vector fields;
std::string current;
bool in_quotes = false;
for (std::size_t i = 0; i < line.size(); ++i) {
char c = line[i];
if (in_quotes) {
if (c == '"') {
if (i + 1 < line.size() && line[i + 1] == '"') {
current += '"'; // an escaped quote
++i;
} else {
in_quotes = false;
}
} else {
current += c;
}
} else if (c == '"') {
in_quotes = true;
} else if (c == ',') {
fields.push_back(current);
current.clear();
} else {
current += c;
}
}
fields.push_back(current);
return fields;
}
int main() {
std::string line = "m-001,\"Smith, Alice\",2024";
for (const std::string& field : parse_csv_line(line)) {
std::cout << "[" << field << "]\n";
}
return 0;
}The output shows the quoted field kept together, with the comma inside it:
[m-001]
[Smith, Alice]
[2024]A real CSV parser also has to handle newlines inside quoted fields, which means reading the whole file and tracking the quote state across lines rather than processing one line at a time.
Converting fields to numbers
std::from_chars is the most reliable way to convert text to a number, because it tells you exactly how much it consumed and it never throws.
#include
#include
#include
#include
std::optional to_int(const std::string& text) {
int value = 0;
const char* first = text.data();
const char* last = text.data() + text.size();
const auto result = std::from_chars(first, last, value);
if (result.ec != std::errc{} || result.ptr != last) {
return std::nullopt;
}
return value;
}
int main() {
std::cout << "42 -> " << to_int("42").value_or(-1) << '\n';
std::cout << "4x -> " << to_int("4x").value_or(-1) << '\n';
std::cout << " -> " << to_int("").value_or(-1) << '\n';
return 0;
}The output shows the clean number parsed and the two malformed ones rejected:
42 -> 42
4x -> -1
-> -1The check on result.ptr is what rejects "4x": the parse succeeded as far as the "4", but there was text left over.
Trimming and normalising
Fields read from a file often carry stray whitespace, and trimming them before comparing is worth doing once in a helper.
#include
#include
#include
std::string trim(const std::string& text) {
const std::string spaces = " \t\r\n";
std::size_t start = text.find_first_not_of(spaces);
if (start == std::string::npos) {
return "";
}
std::size_t end = text.find_last_not_of(spaces);
return text.substr(start, end - start + 1);
}
int main() {
std::cout << "[" << trim(" spaced ") << "]\n";
std::cout << "[" << trim("\t\ttabs\t") << "]\n";
std::cout << "[" << trim(" ") << "]\n";
return 0;
}The output shows the three cases:
[spaced]
[tabs]
[]Reading a whole file into a structure
Putting the pieces together gives a small file reader that turns lines into records.
#include
#include
#include
#include
#include
#include
#include
struct Member {
std::string id;
std::string name;
int year;
};
std::optional to_int(const std::string& text) {
int value = 0;
const char* first = text.data();
const char* last = text.data() + text.size();
const auto result = std::from_chars(first, last, value);
if (result.ec != std::errc{} || result.ptr != last) {
return std::nullopt;
}
return value;
}
std::vector read_members(const std::string& path) {
std::vector members;
std::ifstream file(path);
if (!file) {
return members;
}
std::string line;
while (std::getline(file, line)) {
std::string id;
std::string name;
std::string year_text;
std::istringstream fields(line);
std::getline(fields, id, '|');
std::getline(fields, name, '|');
std::getline(fields, year_text, '|');
auto year = to_int(year_text);
if (year.has_value()) {
members.push_back(Member{id, name, *year});
}
}
return members;
}
int main() {
std::vector members = read_members("members.txt");
std::cout << "loaded " << members.size() << " members\n";
for (const Member& member : members) {
std::cout << member.id << ' ' << member.name << ' ' << member.year << '\n';
}
return 0;
}The output shows the file parsed into records:
loaded 4 members
m-001 Alice 2024
m-002 Bob 2025
m-003 Carol 2026
m-004 Dave 2026Note that a line whose year is not a number is skipped rather than aborting the whole file. That is usually the right behaviour for a batch import.
Quick reference
- Split on a delimiter with
findandsubstr; always push the final field. std::istringstreamhandles whitespace-separated fields and skips extra spaces.- Quoted CSV fields need a state machine; a plain split gets them wrong.
std::from_charsconverts text to numbers and reports what it consumed.- Trim fields once, in a helper, before comparing them.
- Skip malformed records rather than abandoning the whole file.
Try it yourself: write a CSV file with a header row and three records, one of which has a quoted field containing a comma. Read it back and split each line into fields, skipping the header. Then write a function that reports how many fields each record has, and check that the quoted record is counted correctly.