De texte à autre chose, et inversement

Personnellement, je fais ces conversions avec une version « maison » de l'opérateur lexical_cast, inspiré de ce que font les gens de Boost. Pour des détails, voir ../Divers--cplusplus/Casts-maison.html(ou, pour encore plus de détails, voir ../../Sources/lexical_cast_complet.html)

Certaines questions reviennent constamment, dans presque tous les langages de programmation : comment convertir du texte en autre chose (p. ex. : en entier) et inversement?

Dans chaque langage de programmation, on trouve des outils et des stratégies pour faire ce genre d'opération. Nous en présenterons ici quelques unes, soit des stratégies propres :

La liste n'est évidemment pas exhaustive, les techniques proposées pour chaque langage ne le sont pas non plus – l'imagination des informaticiennes et des informaticiens ne connaît, avouons-le, que peu de limites – et la liste n'est présentée dans aucun ordre particulier.

D'autres techniques de conversion, surtout liées aux classes spécifiques à Microsoft, peuvent être consultées ici. Je vous souhaite de ne pas avoir à jouer là-dedans trop souvent, mais je doute que vous y échappiez alors vaut mieux s'armer avec soin.

Considérations générales

Convertir un nombre en texte est une opération relativement simple dans la mesure où l'espace fourni pour représenter le texte en question est suffisamment grand pour y écrire ce texte. Par exemple, pour écrire "3.14159" sous forme ASCIIZ, il importe habituellement d'avoir à sa disposition un espace d'au moins huit caractères, soit un par caractère et un pour le délimiteur marquant la fin du texte.

Cette simplicité est contrebalancée en partie par la différence fondamentale entre nombre et chiffre, le chiffre étant, pour simplifier, l'écriture du nombre. Ainsi, le nombre qu'on comprend habituellement comme 17 sous sa forme décimale peut s'écrire, en C et en C++ sous la forme 0x11, qui est son écriture hexadécimale, et et sous la forme 021, qui est son écriture octale.

Conséquemment, le programme proposé à droite affichera "OUI" à la sortie standard.

#include <iostream>
int main()
{
   using namespace std;
   if (17 == 021 && 17 == 0x11)
      cout << "OUI" << endl;
   else
      cout << "Ceci ne devrait pas se produire" << endl;
}

Je vous invite à consulter ce document sur la structure interne des nombres pour en savoir plus. Évidemment, si on vise à représenter un nombre sous forme de texte, il faudra choisir un système de numération pour l'écriture résultante. La plupart des outils standards utiliseront une écriture décimale par défaut, mais certains (comme itoa(), par exemple, décrit dans la section portant sur le langage C) permettront de choisir la base dans laquelle se fera l'écriture.

En retour, il faut habituellement être prudent pour traduire du texte en nombre, car plusieurs écritures textuelles ne représentent pas des nombres pour la plupart des langages (p. ex. : le texte "Bonjour" se traduit très mal en entier, à moins bien entendu qu'on ait en tête un code un peu spécial). Il n'est pas non plus toujours clair qu'un texte donné puisse se représenter dans le type de données souhaité par le programme (ex: traduire "3.14159" en entier).

Enfin, les questions de bornes (traduire "1000000" en entier codé sur 16 bits, par exemple) doivent être prises en considération.

Langage C

Le langage C utilise des chaînes de caractères ASCIIZ brutes avec des caractères habituellement encodés sur un byte (type char) ou sur un type de 16 bytes ou plus, soit le type wchar_t.

Fonction sprintf()

On peut traduire un nombre en texte en utilisant la fonction sprintf(), comme dans l'exemple à droite.

Cette fonction prend en paramètre, dans l'ordre :

  • un tampon assez grand pour recevoir le texte final (il faut donc prévoir l'espace avec soin!);
  • une chaîne de caractères pouvant contenir des indications de formatage (dans notre exemple : %d pour le int, et %lf pour le float, mais il y a beaucoup de cas possibles); et
  • un nombre variable de paramètres (zéro ou plus) utilisés par la fonction pour remplacer les indications de formatage.
#include <stdio.h> /* en C++: <cstdio> */
int main()
{
   char tampon[256]; /* espérons que ce soit suffisant */
   int i = 3;
   float f = 3.14159f;
   sprintf(
      tampon,
      "Exemple (conversion de nombre en texte): %d %lf\n",
      i,  /* %d, pour «decimal», est utilisé pour les int */
      f   /* %lf sert pour les float */
   );
   /*
     affiche «Exemple (conversion de nombre en texte): 3 3.141590»
   */
   printf(Tampon);
}
Avantages

La fonction fait partie du groupe de base des fonctions d'entrée/ sortie standard du langage C. Elle est donc très connue, très documentée, très utilisée et, dans la plupart des bibliothèques, très optimisée (quoiqu'on puisse parfois faire mieux que les solutions usuelles pour traiter les nombres à virgule flottante).

Certains langages se voulant modernes, comme Java et les langages .NET, offrent des stratégies extrêmement proches de celle-ci, avec les mêmes avantages et (malheureusement) une partie importante des mêmes inconvénients (ci-dessous).

Inconvénients

Comme toutes les fonctions dont le prototype contient une ellipse (un nombre variable de paramètres), le compilateur ne peut aider le programmeur lorsque les indications de la chaîne de formatage ne correspondent pas aux paramètres constituant l'ellipse, que les erreurs en question soient des erreurs sur le nombre de paramètres ou sur le type des paramètres.

Ainsi, le programme ci-dessous est visiblement fautif, mais le problème ne sera relevé que lors de son exécution :

#include <stdio.h> /* ou <cstdio> en C++ */
int main()
{
   char tampon[128];
   sprintf(tampon, "%d %d", 3); /* il manque un paramètre entier... BOUM! */
}
Remarques

Mis à part la fragilité de la démarche, le compilateur ne pouvant raisonnablement pas valider la correspondance entre la chaîne de formatage et le nombre de paramètres à y inscrire, l'autre irritant majeur de cette fonction est qu'elle demande au programmeur de prévoir d'avance la taille du bloc de bytes qui recevra le texte résultant. La plupart des problèmes de débordement de zones tampon (Buffer Overflow) qu'exploitent les programmes hostiles dans Internet profitent à fond de ce type de faiblesse.

Fonction sscanf()

On peut traduire du texte en nombre en utilisant la fonction sscanf(), comme dans l'exemple visible à droite.

Cette fonction prend en paramètre, dans l'ordre :

  • Un texte d'où extraire les nombres
  • Une chaîne de caractères pouvant contenir des indications de formatage (dans notre exemple : %d pour le int, et %lf pour le float, mais il y a beaucoup de cas possibles), et
  • Un nombre variable de paramètres (zéro ou plus) utilisés par la fonction pour recevoir les données extraites du texte. Chacune de ces variables devra être un endroit où pourra écrire la fonction, d'où &i pour l'adresse de i et &f pour l'adresse de f – le langage C ne connaît pas le passage par référence et demande qu'on utilise plutôt le passage par adresse
#include <stdio.h> /* en C++: <cstdio> */
int main()
{
   char *s = "3 3.14159"; /* texte d'où extraire les nombres */
   int i;
   float f;
   sscanf(
      s,
      "%d %lf\n",
      &i,  /* %d, pour «decimal», est utilisé pour les int */
      &f   /* %lf sert pour les float */
   );
   /* affiche «3.141590 3» */
   printf("%lf %d", f, i);
}
Avantages

La fonction fait partie du groupe de base des fonctions d'entrée/ sortie standard du langage C. Elle est donc très connue, très documentée, très utilisée et, dans la plupart des bibliothèques, très optimisée (quoiqu'on puisse parfois faire mieux que les solutions usuelles pour traiter les nombres à virgule flottante).

Certains langages se voulant modernes, comme Java et les langages .NET, offrent des stratégies extrêmement proches de celle-ci, avec les mêmes avantages et (malheureusement) une partie importante des mêmes inconvénients (ci-dessous).

Inconvénients

Comme toutes les fonctions dont le prototype contient une ellipse (un nombre variable de paramètres), le compilateur ne peut aider le programmeur lorsque les indications de la chaîne de formatage ne correspondent pas aux paramètres constituant l'ellipse, que les erreurs en question soient des erreurs sur le nombre de paramètres ou sur le type des paramètres. Ainsi, le programme ci-dessous est visiblement fautif, mais le problème ne sera relevé que lors de son exécution :

#include <stdio.h> /* ou <cstdio> en C+++ */
int main ()
{
   char *s = "allo";
   int i;
   sscanf(s, "%d", &i); /* les types ne correspondent pas... BOUM! */
}

Fonction itoa()

Sous Microsoft Windows, on peut traduire un nombre entier en texte en utilisant la fonction itoa(), comme par exemple :

#include <stdio.h> /* en C++: <cstdio> */
#include <stdlib.h> /* en C++: <cstdlib> */
int main()
{
   int i = 17;
   /* on espère que cela suffise pour contenir le texte résultant */
   char tampon[32];
   /* on peut choisir une base (ici: décimale) */
   int base_voulue = 10;
   itoa(i, tampon, base_voulue);
   /* affiche 17 */
   printf(Tampon);
   base_voulue = 2;
   itoa(i, tampon, base_voulue);
   /* affiche 10001 */
   printf(tampon);
   base_voulue = 16;
   itoa(i, tampon, base_voulue);
   /* affiche 11 */
   printf(tampon);
}

Cette fonction prend en paramètre, dans l'ordre :

Avantages

Cette fonction ne traduit que des entiers, mais peut les écrire sous plusieurs bases différentes. Elle est aussi relativement simple d'usage.

Inconvénients

Cette fonction ne fait pas partie du standard C, et n'est pas portable hors des diverses versions de Microsoft Windows.

Elle requiert une connaissance a priori de l'espace qui sera requis pour écrire l'entier sous la forme choisie. Le compilateur ne peut valider la base demandée – qui doit se situer entre 2 et 36 – à la compilation, ce qui implique un risque plus élevé qu'on ne le souhaiterait d'erreurs à l'exécution.

Fonction atoi()

Sous Microsoft Windows, on peut traduire du texte en nombre entier en utilisant la fonction atoi(), comme par exemple :

#include <stdio.h> /* en C++: <cstdio> */
#include <stdlib.h> /* en C++: <cstdlib> */
int main()
{
   char *s = "17";
   int i = atoi(s);
   /* affiche 17 */
   printf("%d", i);
}

Cette fonction prend en paramètre le texte dont il faut extraire un entier, présumé écrit sous sa forme décimale, et retourne l'entier correspondant.

Avantages

Cette fonction ne traduit que des entiers, mais est très simple d'usage. Nos exemples en C++ s'inspireront de cette simplicité...

Inconvénients

Cette fonction est non portable hors des diverses versions de Microsoft Windows.

Langage C++

Notez que depuis C++ 11, on trouve des fonctions std::to_string() et std::to_wstring() pour convertir de manière standardisée une donnée d'un type quelconque à une std::string ou à une std::wstring. Ces fonctions sont définies pour tous les types primitifs numériques.

Il existe une gamme de fonctions nommées std::stoi() (string to int), std::stof() (string to float), std::strtoull() (string to unsigned long long), etc.

Voir ce texte de 2014 par Marcellus Bancilla pour des détails : http://codexpert.ro/blog/2014/04/14/standard-way-of-converting-between-numbers-and-strings-in-cpp11/

Si vous souhaitez un autre point de vue sur les chaînes de caractères en C++, voir ce texte de John D. Cook : http://www.johndcook.com/cplusplus_strings.html

Le langage C++ permet d'exploiter les outils du langage C, mais offre un paradigme plus intéressant, plus souple et surtout moins dangereux pour arriver au même résultat. On peut ainsi écrire nos propres outils standards de conversion à partir de la paire d'abstractions fort élégante constituée des flux d'entrée/ sortie et des opérateurs d'insertion sur un flux ou d'extraction d'un flux.

Pourquoi préférer std::string à des tableaux de caractères bruts

On voudra presque toujours préférer une std::string à un tableau de caractères bruts, du fait que la std::string peut croître au besoin. Ceci lui confère une souplesse et une élégance qui sont des atouts non négligeables. De manière générale, du fait qu'elle encapsule sa propre gestion de mémoire et qu'elle est consciente de sa propre lonmgueur et de sa propre capacité, il est plus simple de manipuler (copier, passer en paramètre par valeur ou par référence, retourner d'une fonction, concaténer, etc.) une std::string que des blocs de caractères bruts.

Ce qu'est un std::stringstream

Les programmeurs C++ ont un avantage marqué sur les programmeurs C en ce sens que les outils d'entrée/ sortie sur des flux de C++ sont aussi rapides mais plus sécuritaires que ceux de C. En effet, les opérateurs d'insertion dans un flux (<<) et d'extraction d'un flux (>>) de C++ sont des opérateurs binaires (à deux opérandes), dont l'opérande de gauche est un flux et dont l'opérande de droite est – et c'est là quelque chose de merveilleux – une donnée d'un type connu à la compilation.

Impacts de ce constat :

Sachant cela, on comprendra qu'il serait très agréable de pouvoir exploiter un outil de la souplesse et de la puissance des flux d'entrée/ sortie standard pour y insérer ou en extraire des données d'un type arbitraire (mais susceptible d'être écrites sur un flux ou extraites d'un flux).

Et nous avons un tel outil: le std::stringstream, disponible en incluant le fichier <sstream> et pouvant être décliné sous des versions en entrée seule (std::istringstream) ou en sortie seule (std::ostringstream), de manière analogue à tous les flux d'entrée/ sortie standards (incluant les instances de std::istream et de std::ostream que sont respectivement std::cin et std::cout). Le fait que le flux soit représenté sous la forme d'une chaîne de caractères (et n'implique en conséquence aucun accès au système de fichiers) le rend très rapide sans que cela n'entraîne une dégradation du point de vue souplesse.

Les exemples qui suivent montrent comment on peut tirer profit de ce simple mais puissant outil.

Écrire itos()

En C++, la manière la plus simple de traduire un entier en texte est d'écrire l'entier en question sur un flux puis d'extraire le texte contenu dans ce flux, comme on le voit dans l'exemple ci-dessous :

#include <string>
#include <sstream>
using namespace std;
// ...
string itos(int i)
{
   stringstream sstr;
   sstr << i;
   return sstr.str();
}
int main()
{
   cout << "Entrez un entier: " << flush;
   int i;
   if (cin >> i)
   {
      string s = itos(i);
      cout << "Sous forme de texte, cela donne " << s << endl;
   }
}

Écrire stoi()

En C++, la manière la plus simple de traduire du texte en un entier est d'écrire le texte en question sur un flux puis d'extraire l'entier contenu dans ce flux, comme on le voit dans l'exemple ci-dessous :

#include <string>
#include <sstream>
using namespace std;
// ...
int stoi(const string &s)
{
   stringstream sstr;
   sstr << s;
   int i;
   sstr >> i;
   return i;
}
int main()
{
   cout << "Entrez un entier: " << flush;
   string s;
   if (cin >> s)
   {
      int i = stoi(s);
      cout << "J'ai compris " << i << endl;
   }
}

Généraliser (ttos(), stot())

On comprendra rapidement que la stratégie s'applique de manière élégante à tout type pouvant être inscrit sur un flux et extrait d'un flux. Conséquemment, on pourra généraliser itos() et stoi() à tout type T à l'aide de templates :

#include <sstream>
using namespace std;
// ...
template <class T>
   string ttos(const T &val)
   {
      stringstream sstr;
      sstr << val;
      return sstr.str();
   }
template <class T>
   T stot(const string &s)
   {
      stringstream sstr;
      sstr << s;
      T val;
      sstr >> val;
      return val;
   }
int main()
{
   cout << "Entrez un entier: " << flush;
   string s;
   if (cin >> s)
   {
      int i = stot<int>(s);
      cout << "J'ai compris " << i << endl;
      string str = ttos<int>(i);
      cout << "Je confirme " << str <<endl;
   }
}

Pour obtenir le même résultat mais avec une notation telle que celle des conversions explicites de types ISO, voir cet article.

Parenthèse – gérer les erreurs de conversion

Merci à François Boileau pour avoir suggéré ce raffinement.

Si l'objectif est de produire du code de conversion sécuritaire, il faut tenir compte des erreurs de conversion et de leur signalement (par exemple, tenter de traduire "allo" en int est un cas d'exception qui doit être rapporté au sous-programme appelant).

Le problème se résout à peu de frais. En effet, une tentative d'extraction d'une donnée à partir d'un flux, si elle échoue, met le flux dans un état d'erreur qui peut être vérifié en testant ce flux, tout simplement. Ainsi, pour mettre en place une stratégie plus complète de conversion, il suffit d'ajouter un test sur l'état du flux suite à l'extraction et de lever une exception si nécessaire.

#include <sstream>
// ... using et autres trucs ...
class ErreurConversion {};
template <class T>
   string ttos(const T &val)
   {
      stringstream sstr;
      sstr << val;
      return sstr.str();
   }
template <class T>
   T stot(const string &s)
   {
      stringstream sstr;
      sstr << s;
      T val;
      if (!(sstr >> val)) throw ErreurConversion{};
      return val;
   }
int main()
{
   cout << "Entrez un entier: " << flush;
   string s;
   if (cin >> s)
      try
      {
         int i = stot<int>(s);
         cout << "J'ai compris " << i << endl;
         string str = ttos<int>(i);
         cout << "Je confirme " << str << endl;
      }
      catch (ErreurConversion&)
      {
         cerr << "Incapable de convertir " << s << " en entier" << endl;
      }
}

Notez cependant que l'ajout d'un test supplémentaire ralentit le code. Sachant cela, si votre code utilise des données préalablement garanties, en particulier si votre programme est soumis à des contraintes de temps drastiques, vous voudrez peut-être privilégier une approche plus brute.

Avec C++ 11

Depuis C++ 11, certaines fonctionnalités de conversion ont été ajoutées à std::string. Certaines d'entre elles entrâinent d'ailleurs un conflit de nom potentiel avec les exemples de cet article (il existe par exemple maintenant un std::stoi() qui peut être appelée exactement comme l'est la fonction stoi() proposée ici). Pour en savoir plus, voir http://en.cppreference.com/w/cpp/string/basic_string/stol qui en présente une et propose des liens documentant les autres.

Langages .NET (p. ex. : C#, VB.NET)

Dans les langages .NET, la manière privilégiée de traduire une donnée d.un type primitif .NET à un autre type primitif .NET est de passer par System.Convert et d'appeler la méthode de conversion appropriée.

Exemple C++ .NET (désuet – préférez C++/ CLI)
#include "stdafx.h"
#using <mscorlib.dll>
int _tmain()
{
   System::String *s0 = "3";
   int i = System::Convert::ToInt32(s0);
   System::String *s1 = System::Convert::ToString(i);
   System::Console::WriteLine("Chaine {0}: entier {1}", s0, s1);
}
Exemple VB.NET
Module ModuleTest
    Sub Main()
        Dim s0 As String = "3"
        Dim i As Integer = System.Convert.ToInt32(s0)
        Dim s1 As String = System.Convert.ToString(i)
        System.Console.WriteLine("Chaine {0}: entier {1}", s0, s1)
    End Sub
End Module
Exemple C#
namespace Test
{
   public class ClasseTest
   {
      public static void Main()
      {
         string s0 = "3";
         int i = System.Convert.ToInt32(s0);
         string s1 = System.Convert.ToString(i);
         System.Console.WriteLine("Chaine {0}: entier {1}", s0, s1);
         // autre possibilité
         i = int.Parse(s1);
      }
   }
}

Pour des exemples pris directement de la documentation officielle de ce langage (ou de sites qui s'en rapprochent), voir :

Langage Java

Le langage Java a ceci de particulier que, bien qu'il se vante d'être plus objet que la compétition, il offre un type String qui a de particulier d'être à la fois un objet et un type au support proche de celui offert aux types primitifs du langage (incluant un support spécifique pour l'opérateur + dans le but de faciliter la concaténation).

En Java, donc, tout objet se traduit aisément en chaîne de caractères, mais l'inverse demande légèrement plus de travail. Sachant que tout type primitif possède une contrepartie sous forme de classe (p. ex. : le type int et la classe Integer), on peut facilement traduire une donnée simple comme un int en créant un objet (new Integer()) et en le convertissant en String.

À l'inverse, traduire une String en un type primitif comme int demande de passer par une méthode de classe servant de contrepartie OO au type primitif en question et d'appeler la méthode de conversion correspondante.

public class Z {
   public static void main(String [] args) {
      int i = 3;
      String s = new Integer(i).toString();
      int j = Integer.parseInt(s);
   }
}

Il est immédiatement visible que les avantages et les inconvénients de Java et de C# sont à peu près les mêmes : il existe des fonctions de conversion pour les types standards du langage, mais peu de possibilités élégantes d'extension générique de ce modèle.

Pour des exemples pris directement de la documentation officielle de ce langage, voir : http://docs.oracle.com/javase/tutorial/java/data/converting.html


Valid XHTML 1.0 Transitional

CSS Valide !