JavaRush /Курсы /C++ SELF /std::from_chars — парсинг чисел с кодами ошибок

std::from_chars — парсинг чисел с кодами ошибок

C++ SELF
20 уровень , 3 лекция
Открыта

1. Что такое std::from_chars и зачем он нужен

Когда новичок впервые сталкивается с задачей «прочитать число из строки», рука тянется к std::stoi. Это нормально: имя понятное, работает быстро, в примерах в интернете встречается часто. Но у stoi есть характер: при плохом вводе он может бросать исключения (а мы ещё не проходили try/catch), а ещё он живёт в мире строк, где иногда происходят лишние вещи вроде дополнительных проверок/локалей/преобразований.

std::from_chars — это более «низкоуровневый», но очень предсказуемый инструмент: он не бросает исключения, не выделяет память, работает с диапазоном символов [begin, end) и возвращает результат с двумя важными кусочками информации: «почему не получилось» и «на каком символе остановились». В заголовке <charconv> как раз и живёт идея «primitive numeric conversions», где находятся to_chars/from_chars.

Где находится from_chars и как выглядит вызов

Сейчас мы сделаем маленький шаг в сторону «программирования как конструктора»: вместо того чтобы надеяться, что строка «хорошая», мы будем аккуратно разбирать её как последовательность символов.

Для std::from_chars вам почти всегда нужны такие заголовки:

#include <charconv>     // std::from_chars
#include <string_view>  // std::string_view
#include <system_error> // std::errc

from_chars принимает два указателя (да, те самые «адреса», которые мы уже встречали, когда говорили про массивы и идею указателей) и переменную, куда положить результат. То есть логика такая: «Попробуй прочитать число из этого кусочка памяти».

Минимальный пример (строго 7 строк, без лишних спецэффектов):

#include <charconv>
#include <iostream>
#include <string_view>

int main() {
    std::string_view s = "123";
    int x = 0;
    auto r = std::from_chars(s.data(), s.data() + s.size(), x);
    std::cout << x << '\n'; // 123 (но успех мы ещё не проверили!)
}

Обратите внимание на деталь: даже если x вывелся как 123, это ещё не «контракт успеха». У from_chars успех определяется не по принципу «ну вроде похоже», а по коду ошибки в возвращаемом результате.

from_chars_result: два поля ec и ptr

Если воспринимать std::from_chars как мини-автомат, то он делает простую вещь: идёт по символам слева направо, пока может строить число. Потом останавливается и сообщает, что произошло.

Результат имеет тип std::from_chars_result. У него два важных поля:

Поле Смысл по-человечески
ec
код результата (
std::errc{}
= успех, иначе причина ошибки)
ptr
указатель на символ, где парсер остановился

Теперь пример, где мы проверяем именно то, что надо:

#include <charconv>
#include <iostream>
#include <string_view>
#include <system_error>

int main() {
    std::string_view s = "123";
    int x = 0;
    auto r = std::from_chars(s.data(), s.data() + s.size(), x);

    std::cout << (r.ec == std::errc{}) << '\n'; // 1 (успех)
}

Сравнение r.ec == std::errc{} — это официальный стиль «всё хорошо». Да, выглядит странно: будто сравниваем с «пустой ошибкой». Но смысл ровно такой: «ошибки нет».

Про ptr лучше думать как про «курсор». Он указывает на то место, где парсер перестал понимать ввод.

ptr как критерий строгого ввода

Теперь главный практический момент: вы должны решить политику парсинга. Например, строка "12kg" — это число или ошибка?

Если вы пишете калькулятор, возможно, это ошибка. Если вы пишете парсер вида «число в начале строки», возможно, это нормально. from_chars позволяет увидеть оба сценария, потому что ptr покажет, куда дошли.

Пример «число + хвост»:

#include <charconv>
#include <iostream>
#include <string_view>
#include <system_error>

int main() {
    std::string_view s = "12kg";
    int x = 0;
    auto r = std::from_chars(s.data(), s.data() + s.size(), x);

    if (r.ec == std::errc{}) {
        std::cout << x << '\n';      // 12
        std::cout << *r.ptr << '\n'; // k
    }
}

Если вы хотите строгий парсинг («строка должна быть только числом и больше ничем»), то критерий успеха становится двойным:

  1. r.ec == std::errc{}
  2. r.ptr == end (то есть дошли до конца строки)

Вот так это выглядит:

#include <charconv>
#include <iostream>
#include <string_view>
#include <system_error>

int main() {
    std::string_view s = "12kg";
    int x = 0;
    auto r = std::from_chars(s.data(), s.data() + s.size(), x);

    bool ok = (r.ec == std::errc{}) && (r.ptr == s.data() + s.size());
    std::cout << ok << '\n'; // 0
}

И это очень «взрослая» проверка: она не даёт случайно принять мусор как корректные данные.

2. Ошибки парсинга и пробелы

Два базовых кода ошибок

Чтобы ваши сообщения пользователю (или ваши ветки if/else) были осмысленными, нужно различать хотя бы две причины неуспеха.

std::errc::invalid_argument означает «число даже не началось». Типичный пример: строка "abc".

std::errc::result_out_of_range означает «похоже на число, но оно не помещается в тип». Например, если вы парсите в int, а строка — это что-то космическое вроде "999999999999999999999".

Пример, где мы различаем эти случаи:

#include <charconv>
#include <iostream>
#include <string_view>
#include <system_error>

int main() {
    std::string_view s = "999999999999999999999";
    int x = 0;
    auto r = std::from_chars(s.data(), s.data() + s.size(), x);

    std::cout << (r.ec == std::errc::result_out_of_range) << '\n'; // 1
}

Эта часть особенно полезна в учебных CLI-программах: вы можете честно сказать пользователю «это не число» или «число слишком большое для int», вместо «что-то пошло не так».

from_chars не пропускает пробелы

В отличие от std::cin >> x, который обычно пропускает пробелы перед числом, from_chars читает ровно то, что ему дали. Если строка начинается с пробела " 123", то в простом сценарии вы получите invalid_argument.

Это не «плохо» и не «хорошо» — это просто другой контракт. Поэтому в реальном приложении вы обычно либо нормализуете строку заранее (убираете пробелы по краям), либо договариваетесь: «в команде пробелов вокруг числа быть не должно».

Мы сегодня не уходим в полноценную нормализацию (это отдельная тема), поэтому будем придерживаться простого правила: парсим то, что уже выделили как токен без пробелов. Как раз поэтому std::string_view очень удобно использовать вместе с токенизацией: вы берёте кусок строки и парсите его «как есть».

3. Удобные обёртки для приложения

Строгий парсер в std::optional

Продолжим нашу учебную линию: у нас есть простое консольное приложение «список задач», где задачи лежат в std::vector, а пользователь вводит команды. На прошлом занятии мы уже договорились, что «результат может отсутствовать» — это std::optional.

Сделаем функцию parse_int_strict, которая принимает std::string_view и возвращает std::optional<int>: либо число, либо «не получилось».

Сначала схема, чтобы в голове не было каши:

flowchart TD
    A[строка-токен] --> B["from_chars(begin,end,value)"]
    B --> C{"ec == errc{} ?"}
    C -- нет --> D[nullopt]
    C -- да --> E{ptr == end ?}
    E -- нет --> D
    E -- да --> F["optional(value)"]

Теперь реализация. Обратите внимание: код короткий, проверка читается слева направо, «магии» нет.

#include <charconv>
#include <optional>
#include <string_view>
#include <system_error>

std::optional<int> parse_int_strict(std::string_view s) {
    int value = 0;
    auto r = std::from_chars(s.data(), s.data() + s.size(), value);

    if (r.ec != std::errc{}) return std::nullopt;
    if (r.ptr != s.data() + s.size()) return std::nullopt;
    return value;
}

Теперь пример использования в обработчике команды. Допустим, команда удаления задачи выглядит как "del 3", где 3 — id. Мы получили токен "3" и хотим распарсить.

#include <iostream>
#include <optional>
#include <string_view>

std::optional<int> parse_int_strict(std::string_view s);

int main() {
    auto id = parse_int_strict("3");
    if (!id) {
        std::cout << "Bad id\n";
        return 0;
    }
    std::cout << "Deleting task #" << *id << '\n'; // Deleting task #3
}

Заметьте стиль: сначала проверили, потом использовали *id. Это ровно та дисциплина, которую мы тренировали на std::optional.

Расширенный результат со статусом

Иногда optional недостаточно: хочется отличать «не число» от «число с хвостом» от «слишком большое». optional по определению говорит только «есть/нет».

Но нам никто не запрещает вернуть чуть больше информации — например, пару «значение + код ошибки». Пока мы не вводим сложные типы, можно сделать очень простой struct.

#include <charconv>
#include <string_view>
#include <system_error>

struct ParseIntResult {
    int value = 0;
    std::errc ec = std::errc::invalid_argument;
    bool full = false; // дошли ли до конца строки
};

ParseIntResult parse_int(std::string_view s) {
    ParseIntResult out{};
    auto r = std::from_chars(s.data(), s.data() + s.size(), out.value);
    out.ec = r.ec;
    out.full = (r.ptr == s.data() + s.size());
    return out;
}

А теперь «человеческое» сообщение пользователю:

#include <iostream>
#include <string_view>
#include <system_error>

struct ParseIntResult;
ParseIntResult parse_int(std::string_view s);

int main() {
    auto r = parse_int("12kg");

    if (r.ec == std::errc::invalid_argument) {
        std::cout << "Not a number\n";
    } else if (r.ec == std::errc::result_out_of_range) {
        std::cout << "Too big\n";
    } else if (!r.full) {
        std::cout << "Number has tail\n";
    } else {
        std::cout << "Ok: " << r.value << '\n';
    }
}

Этот подход часто спасает UX консольных программ: вместо молчаливого отказа вы объясняете, что именно пользователь ввёл не так. При этом from_chars остаётся вашим «двигателем правды».

4. Шпаргалка по проверкам

Иногда полезно иметь перед глазами маленькую «шпаргалку», чтобы не путаться.

Проверка Что означает Типичный пример ввода
r.ec == std::errc{}
парсер смог прочитать число
"123"
r.ec == std::errc::invalid_argument
число не началось
"abc"
r.ec == std::errc::result_out_of_range
число слишком большое/маленькое для типа
"999999999999"
для
int
r.ptr == end
строка целиком состоит из числа (строгий ввод)
"123"
r.ptr != end
число прочитали, но дальше мусор (хвост)
"12kg"

Самое приятное, что эта логика не зависит от настроения среды выполнения. Она работает предсказуемо. В документах WG21 from_chars/to_chars как раз относятся к «примитивным числовым преобразованиям» в <charconv>, и вокруг них даже обсуждались точечные изменения, например про constexpr для интегральных типов.

5. Типичные ошибки при работе с std::from_chars

Ошибка №1: считать, что если переменная изменилась, значит парсинг успешен.
from_chars может оставить переменную частично заполненной или не тронуть её в значимом для вас смысле — это не критерий. Критерий один: проверка r.ec. Если забыть эту проверку, вы получите «работает на моём вводе» и «ломается на вводе пользователя» — классический сюжет.

Ошибка №2: забыть про проверку ptr и случайно принять «12kg» как корректный ввод.
Многие начинающие проверяют только r.ec, радуются и идут дальше. А потом удивляются, почему команда "del 10abc" внезапно удаляет задачу 10. Если ваша политика — строгий ввод, то проверка r.ptr == end обязательна, иначе вы разрешаете хвост.

Ошибка №3: передавать неправильные границы диапазона [first, last).
from_chars не знает, что такое «строка» — он знает только указатели. Поэтому границы нужно задавать правильно: begin = s.data(), end = s.data() + s.size(). Ошибка в + size() часто превращается в «читаем лишнюю память» или «обрезаем ввод», и потом вы ловите странности, которые тяжело отлаживать.

Ошибка №4: ожидать, что from_chars будет пропускать пробелы, как потоковый ввод.
Он не пропускает. Если вы даёте ему строку с пробелами, результат может быть invalid_argument. Это не баг: это контракт. Либо нормализуйте строку заранее, либо выделяйте токены так, чтобы пробелов там не было.

Ошибка №5: парсить в слишком узкий тип и не обрабатывать result_out_of_range.
Если вы парсите int, то вы обязаны быть готовы к тому, что пользователь введёт число, которое не помещается. И это не «редкий хакерский кейс» — это обычная реальность. В хорошем коде вы различаете «не число» и «слишком большое число», и обрабатываете оба сценария.

1
Задача
C++ SELF, 20 уровень, 3 лекция
Недоступна
Токен контроля
Токен контроля
1
Задача
C++ SELF, 20 уровень, 3 лекция
Недоступна
Диагностика токена
Диагностика токена
1
Задача
C++ SELF, 20 уровень, 3 лекция
Недоступна
Сумма из строки
Сумма из строки
1
Задача
C++ SELF, 20 уровень, 3 лекция
Недоступна
CSV границы
CSV границы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ