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. У него два важных поля:
| Поле | Смысл по-человечески |
|---|---|
|
код результата ( = успех, иначе причина ошибки) |
|
указатель на символ, где парсер остановился |
Теперь пример, где мы проверяем именно то, что надо:
#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
}
}
Если вы хотите строгий парсинг («строка должна быть только числом и больше ничем»), то критерий успеха становится двойным:
- r.ec == std::errc{}
- 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. Шпаргалка по проверкам
Иногда полезно иметь перед глазами маленькую «шпаргалку», чтобы не путаться.
| Проверка | Что означает | Типичный пример ввода |
|---|---|---|
|
парсер смог прочитать число | |
|
число не началось | |
|
число слишком большое/маленькое для типа | для |
|
строка целиком состоит из числа (строгий ввод) | |
|
число прочитали, но дальше мусор (хвост) | |
Самое приятное, что эта логика не зависит от настроения среды выполнения. Она работает предсказуемо. В документах 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, то вы обязаны быть готовы к тому, что пользователь введёт число, которое не помещается. И это не «редкий хакерский кейс» — это обычная реальность. В хорошем коде вы различаете «не число» и «слишком большое число», и обрабатываете оба сценария.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ