Gérer les accents

Imaginons le programme suivant, que bien des programmeurs C et (surtout) C++ reconnaîtront sans peine comme étant un programme lisant le texte d'un fichier, ligne par ligne, et découpant ce texte en un vecteur de mots, le terme mot étant pris ici au sens de séquence de symboles séparés par des blancs, puis affichant à la console le nombre total de mots trouvés:

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <locale>
int main()
{
   using namespace std;
   locale loc{""};
   ifstream ifs{"in.txt"};
   vector<string> v;
   for(string ligne; getline(ifs, ligne);)
   {
      string::size_type cpt = 0;
      while (cpt < ligne.size())
      {
         while (cpt < ligne.size() && isspace(ligne[cpt], loc)) ++cpt;
         string s;
         while (cpt < ligne.size() && !isspace(ligne[Cpt], loc))
         {
            s += ligne[cpt];
            ++cpt;
         }
         if (s.size()) v.push_back(s);
      }
   }
   cout << "Ce fichier contenait " << v.size() << " mots" << endl;
}

Si on considère les fichiers in.txt ci-dessous, ce programme fonctionne très bien avec celui à gauche, mais plante lamentablement aqvec celui à droite.

In.txt (le programme fonctionne) In.txt (le programme plante)
La soupe
de francoise est
pas pire pan toutte 
La soupe
de françoise est
pas pire pan toutte 

Pourquoi? La seule différence entre les deux fichiers tient à l'utilisation d'un caractère « accentué » (le « ç » de Françoise) dans la version de droite, après tout... Pourquoi un si petit changement ferait-il s'écrouler notre programme?

Il se trouve que, sur la plupart des compilateurs C et C++ (mais pas tous), le type char est un type signé, ce qui signifie que la plage de valeurs pouvant être représentées à l'aide de ce type inclut 50% de nombres négatifs – voir Structure interne des nombres pour plus de détails. Ainsi, sur la plupart des compilateurs, un char est un entier signé sur huit bits et peut représenter les caractères à l'aide des valeurs allant de à inclusivement (si le type char est non signé, alors la plage possible va plutôt de 0 à 255 inclusivement).

Les symboles pour lesquels les valeurs entières sont toujours positives (de à ) sont habituellement utilisés, dans un encodage ASCII ou ANSI, pour les caractères connus des américains. Les symboles accentués, comme on les connaît au Québec entre autres, se retrouvent soit entre et (pour un char non signé) ou entre et (pour un char signé).

Les fonctions comme isspace(), qui sont des fonctions du langage C disponibles d'abord à travers <ctype.h> puis (avec C++ ISO) à travers <cctype>, ont été pensées avec la performance en tête – la vitesse d'exécution y prime sur la taille du code. Ainsi, dans leur implémentation, les concepteurs de cette fonction (et de plusieurs autres comme ispunct(), isalpha(), toupper() et ainsi de suite) appliquent la technique d'optimisation par savoir discret, et utilisent la valeur entière du caractère comme index dans un tableau où est a priori inscrit tout le savoir requis pour répondre à la question le caractère c est-il un «blanc» ou non?

Le caractère « ç » dans « Françoise » étant un caractère dont la valeur entière est négative, un appel à isspace() passant ce char en paramètre impliquera la lecture dans un tableau à un index négatif, ce qui est (manifestement) dangereux.

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <cctype>

bool isspace_(char c)
   { return c >= 0 && isspace(c); }
int main()
{
   using namespace std;
   ifstream ifs{ "in.txt" };
   vector<string> v;
   for (string ligne; getline(ifs, ligne); )
   {
      string::size_type cpt = 0;
      while(cpt < ligne.size())
      {
         while(cpt < ligne.size() && isspace_(ligne[cpt]))
            ++cpt;
         string s;
         for(; cpt < ligne.size() && isspace_(ligne[cpt]); ++cpt)
            s += ligne[cpt];
         if (!s.empty()) v.push_back(s);
      }
   }
   cout << "Ce fichier contenait " << v.size() << " mots" << endl;
}

Quelles sont les solutions? Il y en a au moins deux.

  • La première est de valider que la valeur d'un caractère donné soit positive avant d'appeler ::isspace() ou d'autres fonctions du même créneau. Présumant que les blancs sont tous des caractères plutôt conventionnels, on peut donc estimer que tout caractère négatif n'est pas un blanc.
  • La seconde est de migrer vers une version internationnalisée de ::isspace(), soit std::isspace(), tirée de <locale> plutôt que de <cctype>.

La fonction std::isspace() tient compte de la culture locale (obtenue par un std::locale par défaut), et se comporte correctement avectous les caractères de l'alphabet qui y est utilisé.

Voir cette page sur l'internationalisation du code pour plus de détails.

Évidemment, la seconde solution est celle qui devrait être retenue.

#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <locale>

int main()
{
   using namespace std;
   ifstream ifs{ "in.txt" };
   vector<string> v;
   locale loc{""};
   for(string ligne; getline(ifs, ligne); )
   {
      string::size_type cpt = 0;
      while(cpt < ligne.size())
      {
         while(cpt < ligne.size() && isspace(ligne[cpt], loc))
            ++cpt;
         string s;
         for(; cpt < ligne.size() && isspace(ligne[cpt], loc); ++cpt)
            s += ligne[cpt];
         if (!s.empty()) v.push_back(s);
      }
   }
   cout << "Ce fichier contenait " << v.size () << " mots" << endl;
}

Valid XHTML 1.0 Transitional

CSS Valide !