Парсинг текста в C++: как делать правильно
Разбор строк через std::stringstream и ручные циклы даёт лишние копии и падает на кривом вводе. Разбираем std::string_view, std::from_chars и подходы к разбору без аллокаций на C++17 и C++20.

Задача простая: есть строка вида name=Ivan;age=42;city=Moscow, надо разложить её на пары ключ-значение и превратить возраст в число. Первое, что приходит в голову — std::stringstream с getline по разделителю плюс std::stoi. Работает, но на каждый токен вы получаете новый std::string с аллокацией, stoi кидает исключение на мусорном вводе, а stringstream тащит за собой локали и виртуальные вызовы. На разборе одного конфига это незаметно. На разборе миллиона строк лога — разница в разы.
Разберём, как парсить текст в C++17 и C++20 без лишних копий и без сюрпризов на плохом вводе: где брать std::string_view, чем заменить stoi и когда всё-таки нужна полноценная грамматика, а не ручной разбор.
Три уровня задачи — три инструмента
Прежде чем писать код, честно ответьте, что вы парсите. От этого зависит выбор, и подходы не взаимозаменяемы.
- Разбиение по фиксированному разделителю. CSV без кавычек, пары
ключ=значение, список через запятую. Здесь нужен только поиск разделителя и нарезка. - Извлечение чисел и типизированных значений. Строка есть, границы токена известны, надо превратить
"42"вint. Здесь ключевой инструмент —std::from_chars. - Разбор структуры с вложенностью и правилами. JSON, выражения, конфиги с секциями. Ручной разбор здесь превращается в спагетти. Нужна библиотека или генератор парсеров — об этом в конце одной строкой, статья про первые два уровня.
Разбиение по разделителю: string_view вместо копий
Классическая ошибка — резать строку на std::string, хотя исходные данные никуда не денутся до конца обработки. std::string_view (C++17) — это невладеющая пара «указатель + длина». Он не копирует и не аллоцирует, а просто ссылается на кусок исходного буфера.
Функция разбиения на C++17, которая складывает токены во vector<string_view>:
// C++17
#include <string_view>
#include <vector>
std::vector<std::string_view> split(std::string_view text, char delimiter) {
std::vector<std::string_view> tokens;
std::size_t start = 0;
while (start <= text.size()) {
std::size_t pos = text.find(delimiter, start);
if (pos == std::string_view::npos) {
tokens.push_back(text.substr(start));
break;
}
tokens.push_back(text.substr(start, pos - start));
start = pos + 1;
}
return tokens;
}Обратите внимание на substr у string_view: он тоже возвращает string_view, а не копию. Аллокаций внутри цикла нет вообще — только на рост самого вектора.
Главное правило работы с string_view: он живёт ровно столько, сколько живёт буфер, на который он указывает. Вернули string_view на локальную строку — получили висячую ссылку и неопределённое поведение.
Именно поэтому split принимает string_view по значению и не должен получать временный std::string, который умрёт сразу после вызова. Если исходник — временный объект, храните сначала строку, потом режьте.
Числа: from_chars вместо stoi и stringstream
Три способа превратить "42" в число, и только один из них правильный для горячего кода.
std::stoi кидает исключение std::invalid_argument на нечисловом вводе и std::out_of_range на переполнении. Исключение на ожидаемой ситуации (кривой ввод — это норма, а не авария) — дорого и неудобно. К тому же stoi принимает std::string, а не string_view, то есть заставляет копировать.
std::stringstream с operator>> тянет локали, молча проглатывает часть ошибок и медленнее в разы.
std::from_chars (C++17, заголовок <charconv>) не аллоцирует, не кидает исключений, не зависит от локали и возвращает результат через структуру с кодом ошибки:
// C++17
#include <charconv>
#include <string_view>
#include <optional>
std::optional<int> parse_int(std::string_view text) {
int value = 0;
auto [ptr, ec] = std::from_chars(text.data(),
text.data() + text.size(),
value);
// ec == errc{} — успех; ptr должен дойти до конца токена
if (ec == std::errc{} && ptr == text.data() + text.size()) {
return value;
}
return std::nullopt;
}Проверка ptr == text.data() + text.size() важна: from_chars считает "42abc" корректным началом и вернёт 42, остановившись на a. Если вы хотите отвергать хвост — сравнивайте ptr с концом.
Для double перегрузка from_chars с плавающей точкой появилась в стандарте C++17, но полная поддержка в компиляторах пришла позже: в libstdc++ (GCC) — начиная с GCC 11, в MSVC — раньше, в libc++ (Clang) она долго отсутствовала. Перед использованием from_chars для double проверьте свою версию стандартной библиотеки.
Собираем разбор пар ключ-значение
Соединим split и parse_int на исходной задаче name=Ivan;age=42;city=Moscow:
// C++17
#include <string_view>
#include <vector>
#include <utility>
#include <optional>
std::vector<std::pair<std::string_view, std::string_view>>
parse_pairs(std::string_view text) {
std::vector<std::pair<std::string_view, std::string_view>> result;
for (std::string_view field : split(text, ';')) {
std::size_t eq = field.find('=');
if (eq == std::string_view::npos) {
continue; // поле без '=' пропускаем
}
std::string_view key = field.substr(0, eq);
std::string_view val = field.substr(eq + 1);
result.emplace_back(key, val);
}
return result;
}Ни одной строковой аллокации на весь разбор: все ключи и значения — это string_view на исходный буфер. Числа извлекаете уже из нужного значения через parse_int, когда оно вам понадобится.
Сравнение подходов
| Подход | Аллокации на токен | Ошибки | Локаль | Когда уместен |
|---|---|---|---|---|
| stringstream + getline | Да, каждый токен | Молча проглатывает | Зависит | Разовый разбор, читаемость важнее скорости |
| string + stoi | Да | Исключения | Нет для stoi | Маленький ввод, ошибка = авария |
| string_view + from_chars | Нет | Код ошибки, без throw | Нет | Горячий путь, много данных, ожидаемый кривой ввод |
Что ломается на практике
- Висячий string_view. Самая частая и болезненная ошибка. Функция возвращает
string_view, а строка, на которую он ссылался, уже уничтожена. Санитайзер (-fsanitize=address) ловит это на месте — включайте его в тестах. - Хвост после числа.
from_chars("42px")вернёт 42 без ошибки. Если вам нужен строгий разбор — проверяйте, чтоptrдошёл до конца. - Ведущие пробелы.
from_chars, в отличие отstoiиstringstream, не пропускает пробелы в начале." 42"он отвергнет. Обрезайте пробелы сами черезremove_prefix. - Знак у беззнаковых. Для
unsignedперегрузкиfrom_charsведущий минус — ошибка, а не переполнение по модулю. Это правильное поведение, но неочевидное. - CSV с кавычками и экранированием. Ручной
splitпо запятой ломается на"Ivanov, Ivan",42. Как только в формате появляются кавычки, вложенность или экранирование — ручной разбор не масштабируется.
Когда ручной разбор пора бросать
Разбиение по разделителю и извлечение чисел — потолок ручного парсинга. Как только формат обретает грамматику (вложенные структуры, приоритеты, экранирование), пишите не цепочку find и substr, а нормальный парсер.
Для JSON и подобных форматов берите специализированную библиотеку: nlohmann/json для удобства или simdjson для скорости. Для собственных грамматик — библиотеки комбинаторов парсеров или генераторы. Разбор грамматик — отдельная большая тема, здесь мы её только обозначаем.
Что делать дальше
Соберите горячий путь на string_view и from_chars, накройте его тестами с заведомо кривым вводом (пустая строка, поле без разделителя, число с хвостом, переполнение) и прогоните под -fsanitize=address,undefined — именно санитайзер отловит висячие string_view, которые в релизе выстрелят через полгода. Если формат сложнее пар ключ-значение — не растите цепочку find, а сразу берите парсер-библиотеку.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: std::from_chars — cppreference, std::basic_string_view — cppreference
Частые вопросы
У меня старый компилятор без C++17, from_chars недоступен. Что делать?
from_chars для double у меня не компилируется, хотя стандарт C++17. Почему?
Почему split возвращает пустой токен, если строка кончается разделителем?
Опасно ли хранить vector<string_view> в поле класса?
Стоит ли писать собственный парсер CSV или взять готовый?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.