1. Qu'est-ce que std::from_chars et pourquoi l'utiliser
Quand un débutant doit « lire un nombre depuis une chaîne », il a tendance à utiliser std::stoi. C'est normal : le nom est explicite, ça marche vite et on le voit souvent dans les exemples en ligne. Mais stoi a son caractère : en cas d'entrée invalide il peut lancer des exceptions (et nous n'avons pas encore vu try/catch), et il vit dans l'univers des strings où se produisent parfois des choses indésirables comme des contrôles/paramètres régionaux/transforms automatiques.
std::from_chars est un outil plus « bas niveau », mais très prévisible : il ne lance pas d'exceptions, n'alloue pas de mémoire, travaille avec une plage de caractères [begin, end) et retourne un résultat avec deux informations importantes : « pourquoi ça n'a pas marché » et « sur quel caractère il s'est arrêté ». L'en-tête <charconv> contient l'idée des « primitive numeric conversions », où résident to_chars/from_chars.
Où se trouve from_chars et à quoi ressemble l'appel
Ici nous faisons un petit pas vers la « programmation comme construction » : au lieu d'espérer que la chaîne est « correcte », nous la traiterons soigneusement comme une séquence de caractères.
Pour std::from_chars vous aurez presque toujours besoin de ces includes :
#include <charconv> // std::from_chars
#include <string_view> // std::string_view
#include <system_error> // std::errc
from_chars prend deux pointeurs (oui, ces « adresses » que nous avons déjà rencontrées en parlant de tableaux et de pointeurs) et une variable où placer le résultat. La logique est donc : « Essaie de lire un nombre depuis ce segment de mémoire ».
Exemple minimal (strictement 7 lignes, sans effets superflus) :
#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 (mais le succès n'a pas encore été vérifié !)
}
Notez un détail : même si x a affiché 123, ce n'est pas encore un « contrat de succès ». Pour from_chars, le succès est déterminé par le code d'erreur dans le résultat retourné.
from_chars_result : deux champs ec et ptr
Si l'on voit std::from_chars comme un petit automate, il fait une chose simple : il parcourt les caractères de gauche à droite tant qu'il peut construire un nombre. Ensuite il s'arrête et indique ce qui s'est passé.
Le résultat a le type std::from_chars_result. Il comporte deux champs importants :
| Champ | Sens en clair |
|---|---|
|
code du résultat ( = succès, sinon raison de l'erreur) |
|
pointeur vers le caractère où le parseur s'est arrêté |
Maintenant un exemple où l'on vérifie exactement ce qu'il faut :
#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 (succès)
}
La comparaison r.ec == std::errc{} est le style officiel pour dire « tout va bien ». Oui, cela paraît étrange, comme si on comparait à une « erreur vide ». Mais le sens est exactement : « il n'y a pas d'erreur ».
Pensez à ptr comme à un « curseur ». Il indique l'endroit où le parseur a cessé de comprendre l'entrée.
ptr comme critère d'entrée stricte
Voici le point pratique principal : vous devez décider de la politique de parsing. Par exemple, la chaîne "12kg" — est-ce un nombre ou une erreur ?
Si vous écrivez une calculatrice, cela peut être une erreur. Si vous écrivez un parseur qui accepte « nombre en début de chaîne », cela peut être acceptable. from_chars permet de gérer les deux scénarios, car ptr montrera jusqu'où il est allé.
Exemple « nombre + suffixe » :
#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
}
}
Si vous voulez un parsing strict (« la chaîne doit être uniquement un nombre et rien d'autre »), le critère de succès devient double :
- r.ec == std::errc{}
- r.ptr == end (c'est-à-dire qu'on est arrivé à la fin de la chaîne)
Voici à quoi cela ressemble :
#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
}
Et c'est une vérification « mature » : elle évite d'accepter par erreur des données indésirables comme valides.
2. Erreurs de parsing et espaces
Deux codes d'erreur de base
Pour que vos messages à l'utilisateur (ou vos branches if/else) aient du sens, il faut au moins distinguer deux raisons d'échec.
std::errc::invalid_argument signifie « le nombre n'a même pas commencé ». Exemple typique : la chaîne "abc".
std::errc::result_out_of_range signifie « ressemble à un nombre, mais il ne tient pas dans le type ». Par exemple, si vous parsez dans un int et que la chaîne est quelque chose de cosmique comme "999999999999999999999".
Exemple où l'on distingue ces cas :
#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
}
Cette partie est particulièrement utile dans des programmes CLI pédagogiques : vous pouvez dire honnêtement à l'utilisateur « ce n'est pas un nombre » ou « le nombre est trop grand pour int », au lieu de « quelque chose s'est mal passé ».
from_chars ne saute pas les espaces
Contrairement à std::cin >> x, qui ignore habituellement les espaces avant le nombre, from_chars lit exactement ce qu'on lui donne. Si la chaîne commence par un espace " 123", dans un scénario simple vous obtiendrez invalid_argument.
Ce n'est ni « mauvais » ni « bon » — c'est simplement un contrat différent. Donc dans une vraie application vous normalisez généralement la chaîne à l'avance (supprimez les espaces en bordure), ou vous convenez que « il ne doit pas y avoir d'espaces autour du nombre ».
Aujourd'hui nous n'entrons pas dans la normalisation complète (c'est un sujet à part), donc nous suivrons la règle simple : on parse ce qui a été extrait comme token sans espaces. C'est pour cela que std::string_view est pratique avec une tokenisation : vous prenez un segment de la chaîne et vous le parsez « tel quel ».
3. Wrappers pratiques pour l'application
Parseur strict dans std::optional
Continuons la logique pédagogique : nous avons une simple application console « liste de tâches », où les tâches sont dans un std::vector et l'utilisateur saisit des commandes. Lors du cours précédent nous avons convenu que « le résultat peut être absent » — c'est std::optional.
Faisons une fonction parse_int_strict qui prend un std::string_view et retourne std::optional<int> : soit le nombre, soit « échec ».
Schéma d'abord, pour garder les idées claires :
flowchart TD
A[token-de-chaîne] --> B["from_chars(begin,end,value)"]
B --> C{"ec == errc{} ?"}
C -- non --> D[nullopt]
C -- oui --> E{ptr == end ?}
E -- non --> D
E -- oui --> F["optional(value)"]
Maintenant l'implémentation. Notez : le code est court, la vérification se lit de gauche à droite, il n'y a pas de « magie ».
#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;
}
Exemple d'utilisation dans un gestionnaire de commande. Supposons que la commande de suppression ressemble à "del 3", où 3 est l'id. Nous avons obtenu le token "3" et voulons le parser.
#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
}
Remarquez le style : on vérifie d'abord, puis on utilise le *id. C'est la discipline que nous avons pratiquée avec std::optional.
Résultat étendu avec statut
Parfois optional n'est pas suffisant : on veut distinguer « pas un nombre », « nombre avec suffixe » et « trop grand ». optional dit seulement « présent/absent ».
Rien ne vous empêche de retourner un peu plus d'information — par exemple un couple « valeur + code d'erreur ». Avant d'introduire des types complexes, on peut faire une simple struct.
#include <charconv>
#include <string_view>
#include <system_error>
struct ParseIntResult {
int value = 0;
std::errc ec = std::errc::invalid_argument;
bool full = false; // sommes-nous arrivés à la fin de la chaîne
};
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;
}
Et maintenant un message « humain » pour l'utilisateur :
#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';
}
}
Cette approche améliore souvent l'UX des programmes console : au lieu d'un refus silencieux vous expliquez ce qui n'allait pas dans la saisie. Et from_chars reste votre « moteur de vérité ».
4. Aide-mémoire pour les vérifications
Il est utile d'avoir sous les yeux un petit aide-mémoire pour éviter la confusion.
| Vérification | Que cela signifie | Exemple d'entrée typique |
|---|---|---|
|
le parseur a pu lire un nombre | |
|
le nombre n'a pas commencé | |
|
le nombre est trop grand/petit pour le type | pour |
|
la chaîne est entièrement un nombre (entrée stricte) | |
|
le nombre a été lu, mais il reste des caractères (suffixe) | |
Le meilleur, c'est que cette logique ne dépend pas de l'humeur de l'environnement d'exécution. Elle est prédictible. Dans les documents du WG21, from_chars/to_chars font partie des « primitive numeric conversions » dans <charconv>, et des ajustements ponctuels ont même été discutés, par exemple l'ajout de constexpr pour les types entiers.
5. Erreurs typiques en travaillant avec std::from_chars
Erreur n°1 : penser que si la variable a changé alors le parsing a réussi.
from_chars peut laisser la variable partiellement remplie ou ne pas la toucher dans un sens significatif pour vous — ce n'est pas un critère. Le seul critère fiable est la vérification de r.ec. Oublier cette vérification mène au « ça marche avec mon entrée » puis « ça casse avec l'entrée de l'utilisateur » — scénario classique.
Erreur n°2 : oublier de vérifier ptr et accepter par inadvertance « 12kg » comme valide.
Beaucoup vérifient seulement r.ec, se réjouissent et passent à autre chose. Puis ils s'étonnent que la commande "del 10abc" supprime la tâche 10. Si votre politique est l'entrée stricte, la vérification r.ptr == end est obligatoire, sinon vous autorisez des suffixes.
Erreur n°3 : passer des bornes incorrectes pour la plage [first, last).
from_chars ne connaît pas le concept de « chaîne » — il ne voit que des pointeurs. Donc fixez correctement les bornes : begin = s.data(), end = s.data() + s.size(). Une erreur sur le + avec size() conduit souvent à « lire de la mémoire en trop » ou à « tronquer l'entrée », ce qui donne des bugs difficiles à déboguer.
Erreur n°4 : s'attendre à ce que from_chars ignore les espaces comme l'entrée stream.
Il ne les ignore pas. Si vous lui donnez une chaîne avec des espaces, le résultat peut être invalid_argument. Ce n'est pas un bug : c'est le contrat. Soit normalisez la chaîne à l'avance, soit extrayez des tokens sans espaces.
Erreur n°5 : parser dans un type trop étroit et ne pas gérer result_out_of_range.
Si vous parsez dans un int, vous devez être prêt à ce que l'utilisateur saisisse un nombre qui ne rentre pas. Ce n'est pas un « cas rare » mais une réalité courante. Un bon code distingue « pas un nombre » et « nombre trop grand » et traite les deux cas.
GO TO FULL VERSION