web-dev-qa-db-fra.com

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

J'ai le morceau de code suivant qui invite l'utilisateur pour 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?

81
0x499602D2

Pourquoi cela arrive-t-il?

Cela a peu à voir avec l'entrée que vous avez vous-même fournie mais plutôt avec le comportement par défaut std::getline(). 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 Enter ou Return lors de la soumission d'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 dans namejusqu'à la prochaine opération d'E/S où elle est ignorée ou consommée. Lorsque le flux de contrôle atteint std::getline(), la nouvelle ligne est supprimée, mais l'entrée cesse immédiatement. Cela s’explique par le fait que la fonctionnalité par défaut de cette fonction l’impose (il tente de lire une ligne et s’arrête dès qu’il trouve une nouvelle ligne).

Parce que cette nouvelle ligne principale inhibe la fonctionnalité attendue de votre programme, il s'ensuit que nous devons l'ignorer 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 de sorte que la nouvelle ligne ne soit plus intrusive.


Explication détaillée:

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

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 charTname__. 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’a pas été fourni.

Maintenant, voici quelques-unes des conditions dans lesquelles std::getline() termine l’entrée:

  • Si le flux a extrait le nombre maximal de caractères, un 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 à laquelle nous avons affaire. Votre entrée dans stateest représentée ainsi:

"John\nNew Hampshire"
     ^
     |
 next_pointer

next_pointer est 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() l'ignore discrètement, incrémente next_pointer jusqu'au prochain caractère disponible et arrête la saisie. Cela signifie que les autres 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 dans statename__, votre extraction produira le résultat correct au dernier appel de std::getline() avec le délimiteur ignoré.


Vous avez peut-être remarqué que ce problème ne survient généralement pas 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 séparateurs et ont le std::skipws1 manipulateur activé par défaut. Les flux élimineront les espaces de début du flux lorsque vous commencerez à 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 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 en état d'échec ou non. Vous constaterez seulement que dans les fonctions d'entrée non formatées , le deuxième argument du constructeur sentry est truename__. Cet argument signifie que les espaces 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 noskipwsvaut zéro et is.flags() & ios_base::skipws est différent de zéro, la fonction extrait et supprime chaque caractère tant que le prochain caractère en entrée disponible cest un caractère d'espacement. [...]

Puisque la condition ci-dessus est fausse, l'objet sentinelle ne supprimera pas les espaces. La raison pour laquelle noskipwsest défini sur truepar cette fonction est parce que le point de std::getline() est de lire des caractères bruts non formatés dans un objet std::basic_string<charT>.


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 que std::getline() ne soit exécuté (mais faites-le après l'extraction formatée). Cela peut être fait en utilisant ignore() pour ignorer le reste de l'entrée jusqu'à atteindre une nouvelle ligne:

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

Vous devez 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 fonction max() renvoie le plus grand nombre de caractères qu'un flux peut accepter.

Une autre façon de supprimer les espaces blancs consiste à utiliser la fonction std::ws, qui est un manipulateur conçu pour extraire et ignorer les espaces principaux à partir du 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 indifféremment des caractères jusqu'à ce qu'il supprime countname__, trouve le délimiteur (spécifié par le deuxième argument delimname__) ou touche la fin du flux. std::ws est uniquement utilisé pour ignorer les espaces depuis le début du flux.

Si vous mélangez des entrées formatées avec des entrées non formatées et que vous devez supprimer les espaces blancs résiduels, utilisez std::ws. Sinon, si vous devez effacer une entrée non valide, quelle qu’elle soit, utilisez ignore(). Dans notre exemple, nous devons uniquement effacer les espaces, car le flux a utilisé votre entrée de "John" pour la variable namename__. Il ne restait que le caractère de nouvelle ligne.


1: std::skipws est un manipulateur qui indique au flux d'entrée de supprimer les espaces blancs les plus importants lors de la saisie d'une entrée formatée. Ceci peut être désactivé avec le manipulateur std::noskipws.

2: les flux d'entrée considèrent certains caractères comme des espaces par défaut, tels que le caractère d'espace, le caractère de nouvelle ligne, le saut de page, le retour à la ligne, 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 une certaine quantité de caractères ou deux arguments pour ignorer les caractères countou jusqu'à ce qu'il atteigne delimname__, selon la première éventualité. Vous utilisez normalement std::numeric_limits<std::streamsize>::max() comme valeur de countsi vous ne connaissez pas le nombre de caractères avant le délimiteur, mais vous souhaitez néanmoins les supprimer.

103
0x499602D2

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

if ((cin >> name).get() && std::getline(cin, state))
10
Boris

Cela est dû au fait qu'un saut de ligne implicite, également appelé caractère de nouvelle ligne \n, est ajouté à toutes les entrées utilisateur d'un terminal, car il indique au flux de commencer une nouvelle ligne. Vous pouvez en toute sécurité en tenir compte en utilisant std::getline lors de la vérification de plusieurs lignes de saisie utilisateur. Le comportement par défaut de std::getline lira tout, y compris le caractère de nouvelle ligne \n, à partir de l'objet de flux d'entrée qui est std::cin dans 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"
0
Justin Randall