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 << "Некоректний id\n";
return 0;
}
std::cout << "Видаляємо завдання #" << *id << '\n'; // Видаляємо завдання #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 << "Це не число\n";
} else if (r.ec == std::errc::result_out_of_range) {
std::cout << "Число надто велике\n";
} else if (!r.full) {
std::cout << "Після числа є зайві символи\n";
} else {
std::cout << "Гаразд: " << r.value << '\n';
}
}
Цей підхід часто робить консольні програми зручнішими: замість мовчазної відмови ви пояснюєте, що саме користувач увів не так. При цьому 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, то маєте бути готові до того, що користувач уведе число, яке не вміщується. І це не «рідкісний хакерський кейс», а звична реальність. У хорошому коді ви розрізняєте «не число» і «надто велике число» та обробляєте обидва сценарії.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ