Pourquoi std :: getline () ignore-t-il l'entrée après une extraction formatée?

105

J'ai le morceau de code suivant qui demande à l'utilisateur son nom et son état:

#include <iostream>
#include <string>

int main()
{
    std::string name;
    std::string state;

    if (std::cin >> name && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
}

Ce que je trouve, c'est que le nom a été extrait avec succès, mais pas l'état. Voici l'entrée et la sortie résultante:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Pourquoi le nom de l'état a-t-il été omis de la sortie? J'ai donné la bonne entrée, mais le code l'ignore en quelque sorte. Pourquoi cela arrive-t-il?

0x499602D2
la source
Je pense qu'il std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)devrait également fonctionner comme prévu. (En plus des réponses ci-dessous).
jww

Réponses:

122

Pourquoi cela arrive-t-il?

Cela n'a pas grand-chose à voir avec l'entrée que vous avez fournie vous-même mais plutôt avec les std::getline()expositions de comportement par défaut . Lorsque vous avez fourni votre entrée pour le nom ( std::cin >> name), vous avez non seulement soumis les caractères suivants, mais également une nouvelle ligne implicite a été ajoutée au flux:

"John\n"

Une nouvelle ligne est toujours ajoutée à votre entrée lorsque vous sélectionnez Enterou Returnlors de la soumission depuis un terminal. Il est également utilisé dans les fichiers pour passer à la ligne suivante. La nouvelle ligne est laissée dans la mémoire tampon après l'extraction namejusqu'à la prochaine opération d'E / S où elle est soit rejetée, soit consommée. Lorsque le flux de contrôle atteint std::getline(), la nouvelle ligne sera rejetée, mais l'entrée cessera immédiatement. La raison pour laquelle cela se produit est que la fonctionnalité par défaut de cette fonction l'exige (elle tente de lire une ligne et s'arrête lorsqu'elle trouve une nouvelle ligne).

Étant donné que cette nouvelle ligne principale inhibe la fonctionnalité attendue de votre programme, il s'ensuit qu'elle doit être ignorée d'une manière ou d'une autre. Une option consiste à appeler std::cin.ignore()après la première extraction. Il supprimera le prochain caractère disponible pour que la nouvelle ligne ne gêne plus.

std::getline(std::cin.ignore(), state)

Explication détaillée:

C'est la surcharge de ce std::getline()que vous avez appelé:

template<class charT>
std::basic_istream<charT>& getline( std::basic_istream<charT>& input,
                                    std::basic_string<charT>& str )

Une autre surcharge de cette fonction prend un délimiteur de type charT. Un caractère délimiteur est un caractère qui représente la limite entre les séquences d'entrée. Cette surcharge particulière définit le délimiteur sur le caractère de nouvelle ligne input.widen('\n')par défaut, car il n'en a pas été fourni.

Voici quelques-unes des conditions dans lesquelles se std::getline()termine l'entrée:

  • Si le flux a extrait le nombre maximum de caractères que std::basic_string<charT>peut contenir
  • Si le caractère de fin de fichier (EOF) a été trouvé
  • Si le délimiteur a été trouvé

La troisième condition est celle dont nous traitons. Votre entrée dans stateest représentée ainsi:

"John\nNew Hampshire"
     ^
     |
 next_pointer

next_pointerest le prochain caractère à analyser. Étant donné que le caractère stocké à la position suivante dans la séquence d'entrée est le délimiteur, std::getline()supprime silencieusement ce caractère, passe next_pointerau caractère disponible suivant et arrête l'entrée. Cela signifie que le reste des caractères que vous avez fournis restent dans la mémoire tampon pour la prochaine opération d'E / S. Vous remarquerez que si vous effectuez une autre lecture de la ligne vers state, votre extraction donnera le résultat correct comme dernier appel à std::getline()ignorer le délimiteur.


Vous avez peut-être remarqué que vous ne rencontrez généralement pas ce problème lors de l'extraction avec l'opérateur d'entrée formaté ( operator>>()). Cela est dû au fait que les flux d'entrée utilisent des espaces comme délimiteurs pour l'entrée et que le manipulateur std::skipws1 est activé par défaut. Streams supprimera le premier espace blanc du flux lorsqu'il commence à effectuer une entrée formatée. 2

Contrairement aux opérateurs d'entrée formatés, std::getline()est une fonction d'entrée non formatée . Et toutes les fonctions d'entrée non formatées ont le code suivant quelque peu en commun:

typename std::basic_istream<charT>::sentry ok(istream_object, true);

Ce qui précède est un objet sentinelle qui est instancié dans toutes les fonctions d'E / S formatées / non formatées dans une implémentation C ++ standard. Les objets Sentry sont utilisés pour préparer le flux pour les E / S et déterminer s'il est ou non en état d'échec. Vous constaterez uniquement que dans les fonctions d'entrée non formatées , le deuxième argument du constructeur sentinelle est true. Cet argument signifie que les espaces blancs de début ne seront pas supprimés à partir du début de la séquence d'entrée. Voici la citation pertinente de la norme [§27.7.2.1.3 / 2]:

 explicit sentry(basic_istream<charT, traits>& is, bool noskipws = false);

[...] Si noskipwsest égal à zéro et is.flags() & ios_base::skipwsdifférent de zéro, la fonction extrait et supprime chaque caractère tant que le prochain caractère d'entrée disponible cest un caractère d'espacement. [...]

Puisque la condition ci-dessus est fausse, l'objet sentinelle ne rejettera pas l'espace blanc. La raison noskipwsest définie truepar cette fonction est que le but de std::getline()est de lire des caractères bruts et non formatés dans un std::basic_string<charT>objet.


La solution:

Il n'y a aucun moyen d'arrêter ce comportement de std::getline(). Ce que vous devrez faire est de supprimer vous-même la nouvelle ligne avant l' std::getline()exécution (mais faites-le après l'extraction formatée). Cela peut être fait en utilisant ignore()pour supprimer le reste de l'entrée jusqu'à ce que nous atteignions une nouvelle ligne:

if (std::cin >> name &&
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n') &&
    std::getline(std::cin, state))
{ ... }

Vous devrez inclure <limits>pour utiliser std::numeric_limits. std::basic_istream<...>::ignore()est une fonction qui supprime un nombre spécifié de caractères jusqu'à ce qu'elle trouve un délimiteur ou atteigne la fin du flux ( ignore()supprime également le délimiteur s'il le trouve). La max()fonction renvoie le plus grand nombre de caractères qu'un flux peut accepter.

Une autre façon de supprimer l'espace blanc est d'utiliser la std::wsfonction qui est un manipulateur conçu pour extraire et supprimer les espaces blancs de début depuis le début d'un flux d'entrée:

if (std::cin >> name && std::getline(std::cin >> std::ws, state))
{ ... }

Quelle est la différence?

La différence est que ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 supprime les caractères sans discernement jusqu'à ce qu'il rejette les countcaractères, trouve le délimiteur (spécifié par le deuxième argument delim) ou atteigne la fin du flux. std::wsest uniquement utilisé pour supprimer les caractères d'espacement depuis le début du flux.

Si vous mélangez une entrée formatée avec une entrée non formatée et que vous devez supprimer les espaces blancs résiduels, utilisez std::ws. Sinon, si vous devez effacer les entrées non valides quelle que soit leur nature, utilisez ignore(). Dans notre exemple, nous n'avons besoin que d'effacer les espaces puisque le flux a consommé votre entrée de "John"pour la namevariable. Tout ce qui restait était le caractère de nouvelle ligne.


1: std::skipwsest un manipulateur qui indique au flux d'entrée de supprimer les espaces de début lors de l'exécution d'une entrée formatée. Cela peut être désactivé avec le std::noskipwsmanipulateur.

2: Les flux d'entrée considèrent certains caractères comme des espaces par défaut, tels que le caractère espace, le caractère de nouvelle ligne, le saut de page, le retour chariot, etc.

3: Ceci est la signature de std::basic_istream<...>::ignore(). Vous pouvez l'appeler avec zéro argument pour supprimer un seul caractère du flux, un argument pour supprimer un certain nombre de caractères ou deux arguments pour supprimer des countcaractères ou jusqu'à ce qu'il atteigne delim, selon le premier des deux. Vous utilisez normalement std::numeric_limits<std::streamsize>::max()comme valeur de countsi vous ne savez pas combien de caractères il y a avant le délimiteur, mais vous voulez quand même les ignorer.

0x499602D2
la source
1
Pourquoi pas simplement if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
@FredLarson Bon point. Bien que cela ne fonctionne pas si la première extraction est un entier ou tout ce qui n'est pas une chaîne.
0x499602D2
Bien sûr, ce n'est pas le cas ici et il ne sert à rien de faire la même chose de deux manières différentes. Pour un entier, vous pouvez obtenir la ligne dans une chaîne puis l'utiliser std::stoi(), mais ce n'est pas si clair qu'il y a un avantage. Mais j'ai tendance à préférer uniquement utiliser std::getline()pour une entrée orientée ligne, puis à analyser la ligne de la manière qui a du sens. Je pense que c'est moins sujet aux erreurs.
Fred Larson
@FredLarson D'accord. J'ajouterai peut-être cela si j'ai le temps.
0x499602D2
1
@Albin La raison pour laquelle vous voudrez peut-être utiliser std::getline()est si vous voulez capturer tous les caractères jusqu'à un délimiteur donné et le saisir dans une chaîne, par défaut, c'est la nouvelle ligne. Si ce Xnombre de chaînes ne sont que des mots / jetons uniques, ce travail peut être facilement accompli avec >>. Sinon, vous saisissez le premier nombre dans un entier avec >>, appelez cin.ignore()sur la ligne suivante, puis exécutez une boucle où vous utilisez getline().
0x499602D2 le
11

Tout ira bien si vous modifiez votre code initial de la manière suivante:

if ((cin >> name).get() && std::getline(cin, state))
Boris
la source
3
Je vous remercie. Cela fonctionnera également car get()consomme le personnage suivant. Il y a aussi (std::cin >> name).ignore()ce que j'ai suggéré plus tôt dans ma réponse.
0x499602D2
".. travailler parce que get () ..." Oui, exactement. Désolé d'avoir donné la réponse sans détails.
Boris
4
Pourquoi pas simplement if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
0

Cela se produit car un saut de ligne implicite également connu sous le nom de caractère de nouvelle ligne \nest ajouté à toutes les entrées utilisateur d'un terminal car il indique au flux de commencer une nouvelle ligne. Vous pouvez en tenir compte en toute sécurité en utilisant std::getlinelors de la vérification de plusieurs lignes d'entrée utilisateur. Le comportement par défaut de std::getlinelira tout jusqu'à et y compris le caractère \nde nouvelle ligne de l'objet de flux d'entrée, ce qui est std::cindans ce cas.

#include <iostream>
#include <string>

int main()
{
    std::string name;
    std::string state;

    if (std::getline(std::cin, name) && std::getline(std::cin, state))
    {
        std::cout << "Your name is " << name << " and you live in " << state;
    }
    return 0;
}
Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in New Hampshire"
Justin Randall
la source