Comprendre et gérer les problèmes avec std::getline()

Le langage C++ offre une variété d'outils d'entrée/ sortie sur des flux de données, et utilise les classes std::istream et std::ostream comme abstractions fondamentales pour représenter (respectivement) les flux en entrée et les flux en sortie.

Appliquant héritage et polymorphisme, ceci permet de gérer sur un même pied les entrées/ sorties à la console (avec, en particulier, std::cin, une instance de std::istream liée à l'entrée standard – typiquement le clavier – et std::cout, une instance de std::ostream liée à la sortie standard – typiquement l'écran en mode console), sur un lien de communication, sur un fichier ou sur tout conteneur respectueux de certaines règles de base.

Pour l'entrée de chaînes de caractères, on aura tendance à utiliser l'opérateur >> tel que défini sur un opérande de gauche de type std::istream& et sur un opérande de droite de type std::string&, profitant ainsi de la grande souplesse du type std::string, dont les instances ont une capacité d'entreposage capable de croître en fonction des besoins (ce qui élimine les problèmes de dépassement de capacité des zones tampons, aussi nommées Buffer Overflow Problems).

#include <string>
#include <fstream>
#include <iostream>
// lit chaque mot d'un fichier texte et les affiche
// à la sortie standard, séparés par des tabulations
int main() {
   using namespace std;
   ifstream entree{ "in.txt" };
   for (string mot; entree >> mot`)
      cout << mot << '\t';
}

L'opérateur >> sur un flux en entrée et une std::string a pour comportement de lire sur le flux jusqu'à la rencontre du premier caractère d'espacement.

Si on désire lire une ligne à la fois, par exemple dans un cas où les espaces sont significatifs, alors ce n'est pas le meilleur outil.

Dans ces circonstances, on a habituellement recours à std::getline().

Il se trouve que std::getline() est une fonction très efficace qui prend en paramètre un std::istream& et un std::string& et qui lit des caractères du flux jusqu'à un délimiteur de fin de chaîne (on peut décider soi-même de lire jusqu'à un caractère particulier qui soit différent de '\n' en insérant le délimiteur désiré comme troisième paramètre de la fonction).

Il est très fréquent que des étudiants manipulant ces deux outils rencontrent des problèmes à l'usage de std::getline(). En effet, dans un programme comme celui-ci, la lecture de la chaîne s va être escamotée.

#include <string>
#include <fstream>
#include <iostream>
// lit chaque ligne d'un fichier texte et les affiche
// à la sortie standard, séparées par des sauts de ligne
int main() {
   using namespace std;
   ifstream entree{"in.txt"};
   for (string ligne; getline(entree, ligne); )
      cout << ligne << endl;
}

On peut comprendre ce qui se passe en allant tracer le code de std::getline(), qui est un template exposé dans le fichier d'en-tête <string>, mais voici en gros ce qui se produit :

  • Lorsqu'on lit le premier entier (val), l'entier en soi est lu, mais std::cin garde dans son tampon interne le caractère de nouvelle ligne ('\n') ayant servi à compléter l'entrée de données, parce que ce caractère ne fait pas partie de l'entier lu et parce qu'il pourrait être utile lors de la prochaine lecture (std::cin ne sait pas que nous, le '\n', on n'en veut pas)
  • Par défaut (et c'est ce qu'on veut), std::getline() lit ce qui apparaît dans le tampon du flux en entrée reçu en paramètre (ici: std::cin) jusqu'à ce qu'apparaisse un premier '\n'. Dans notre cas, s'il traîne un tel caractère dans le tampon, c'est moche
  • Le truc élégant est de vérifier au besoin s'il y a, au début du tampon du flux en entrée utilisé pour std::getline() (donc au début du tampon de std::cin), un changement de ligne ('\n') ou un retour de chariot ('\r') qui traîne dans le cas où on sait qu'il est possible qu'on ait laissé traîner un tel caractère, et de l'enlever du flux s'il y a lieu
#include <string>
#include <iostream>
int main() {
   using namespace std;
   cout << "Entrez un entier...";
   if (int val; cin >> val) {
      cout << "Entrez une ligne de texte...";
      // lire une ligne à l'entrée standard, la mettre dans s
      if (string s; getline(cin, s))
         cout << s << endl;
   }
}

Je vous propose donc la version ci-dessous, qui (elle) fonctionne bien :

#include <string>
#include <iostream>
int main() {
   using namespace std;
   cout << "Entrez un entier...";
   int val;
   cin >> val;
   cout << "Entrez une ligne de texte...";
   if (char c = cin.peek(); c == '\n' || c == '\r')
      cin.get(c);
   // lire une ligne à l'entrée standard, la mettre dans s
   if (string s; getline(cin, s))
      cout << s << endl;
}

L'appel à std::cin.peek() retourne le premier caractère au début du flux en entrée. S'il s'agit d'une fin de ligne, alors on le retire du flux. Il est important de vérifier la présence ou non d'un tel caractère, d'ailleurs, car deux cas sont possibles :

Solution plus simple encore, si le contenu du tampon en entrée peut être éliminé : appeler la méthode ignore() qui « oublie » que le contenu du tampon existe, pour essentiellement repartir à neuf :

#include <string>
#include <iostream>
int main() {
   using namespace std;
   cout << "Entrez un entier...";
   int val;
   cin >> val;
   cout << "Entrez une ligne de texte...";
   cin.ignore();
   if (string s; getline(cin, s))
      cout << s << endl;
}

Voilà!

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !