¿Por qué std :: getline () omite la entrada después de una extracción formateada?

105

Tengo el siguiente fragmento de código que solicita al usuario su nombre y estado:

#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;
    }
}

Lo que encuentro es que el nombre se ha extraído con éxito, pero no el estado. Aquí está la entrada y la salida resultante:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

¿Por qué se ha omitido el nombre del estado en la salida? He dado la entrada adecuada, pero el código de alguna manera lo ignora. ¿Por qué pasó esto?

0x499602D2
fuente
Creo std::cin >> name && std::cin >> std::skipws && std::getline(std::cin, state)que también debería funcionar como se esperaba. (Además de las respuestas a continuación).
jww

Respuestas:

122

¿Por qué pasó esto?

Esto tiene poco que ver con la información que proporcionó usted mismo, sino más bien con las std::getline()exhibiciones de comportamiento predeterminado . Cuando proporcionó su entrada para el nombre ( std::cin >> name), no solo envió los siguientes caracteres, sino que también se agregó una nueva línea implícita a la secuencia:

"John\n"

Siempre se agrega una nueva línea a su entrada cuando selecciona Entero Returncuando envía desde una terminal. También se utiliza en archivos para avanzar hacia la siguiente línea. La nueva línea se deja en el búfer después de la extracción namehasta la siguiente operación de E / S en la que se descarta o se consume. Cuando llegue el flujo de control std::getline(), se descartará la nueva línea, pero la entrada cesará inmediatamente. La razón por la que esto sucede es porque la funcionalidad predeterminada de esta función dicta que debería (intenta leer una línea y se detiene cuando encuentra una nueva línea).

Debido a que esta nueva línea principal inhibe la funcionalidad esperada de su programa, se deduce que debe omitirse o ignorarse de alguna manera. Una opción es llamar std::cin.ignore()después de la primera extracción. Descartará el siguiente carácter disponible para que la nueva línea ya no esté en el camino.

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

Explicación en profundidad:

Esta es la sobrecarga de lo std::getline()que llamaste:

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

Otra sobrecarga de esta función toma un delimitador de tipo charT. Un carácter delimitador es un carácter que representa el límite entre las secuencias de entrada. Esta sobrecarga en particular establece el delimitador en el carácter de nueva línea input.widen('\n')de forma predeterminada ya que no se proporcionó uno.

Ahora, estas son algunas de las condiciones por las cuales std::getline()termina la entrada:

  • Si la transmisión ha extraído la cantidad máxima de caracteres que std::basic_string<charT>puede contener
  • Si se ha encontrado el carácter de fin de archivo (EOF)
  • Si se ha encontrado el delimitador

La tercera condición es la que estamos tratando. Su entrada en statese representa así:

"John\nNew Hampshire"
     ^
     |
 next_pointer

donde next_pointeres el siguiente carácter que se analizará. Dado que el carácter almacenado en la siguiente posición en la secuencia de entrada es el delimitador, std::getline()descartará silenciosamente ese carácter, incrementará next_pointeral siguiente carácter disponible y detendrá la entrada. Esto significa que el resto de los caracteres que ha proporcionado aún permanecen en el búfer para la siguiente operación de E / S. Notará que si realiza otra lectura desde la línea hacia state, su extracción producirá el resultado correcto como la última llamada para std::getline()descartar el delimitador.


Es posible que haya notado que normalmente no se encuentra con este problema al extraer con el operador de entrada formateado ( operator>>()). Esto se debe a que los flujos de entrada utilizan espacios en blanco como delimitadores para la entrada y tienen el manipulador std::skipws1 activado de forma predeterminada. Las transmisiones descartarán los espacios en blanco iniciales de la transmisión cuando comience a realizar una entrada formateada. 2

A diferencia de los operadores de entrada formateados, std::getline()es una función de entrada sin formato . Y todas las funciones de entrada sin formato tienen algo en común el siguiente código:

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

Lo anterior es un objeto centinela que se instancia en todas las funciones de E / S formateadas / no formateadas en una implementación estándar de C ++. Los objetos centinela se utilizan para preparar el flujo para E / S y determinar si está en un estado de falla o no. Solo encontrará que en las funciones de entrada sin formato , el segundo argumento del constructor sentry es true. Ese argumento significa que los espacios en blanco iniciales no se descartarán desde el principio de la secuencia de entrada. Aquí está la cita relevante de la Norma [§27.7.2.1.3 / 2]:

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

[...] Si noskipwses cero y is.flags() & ios_base::skipwses distinto de cero, la función extrae y descarta cada carácter siempre que el siguiente carácter de entrada disponible csea ​​un carácter de espacio en blanco. [...]

Dado que la condición anterior es falsa, el objeto centinela no descartará el espacio en blanco. La razón noskipwsque establece trueesta función es que el objetivo de std::getline()es leer caracteres sin formato y sin formato en un std::basic_string<charT>objeto.


La solución:

No hay forma de detener este comportamiento de std::getline(). Lo que tendrá que hacer es descartar la nueva línea usted mismo antes de que se std::getline()ejecute (pero hágalo después de la extracción formateada). Esto se puede hacer usando ignore()para descartar el resto de la entrada hasta que alcancemos una nueva línea nueva:

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

Deberá incluir <limits>para usar std::numeric_limits. std::basic_istream<...>::ignore()es una función que descarta una cantidad específica de caracteres hasta que encuentra un delimitador o llega al final de la secuencia ( ignore()también descarta el delimitador si lo encuentra). La max()función devuelve la mayor cantidad de caracteres que puede aceptar una secuencia.

Otra forma de descartar los espacios en blanco es usar la std::wsfunción que es un manipulador diseñado para extraer y descartar los espacios en blanco iniciales desde el principio de un flujo de entrada:

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

¿Cual es la diferencia?

La diferencia es que ignore(std::streamsize count = 1, int_type delim = Traits::eof())3 descarta caracteres indiscriminadamente hasta que descarta countcaracteres, encuentra el delimitador (especificado por el segundo argumento delim) o llega al final de la secuencia. std::wssolo se utiliza para descartar caracteres de espacio en blanco desde el principio de la transmisión.

Si está mezclando una entrada formateada con una entrada sin formato y necesita descartar los espacios en blanco residuales, use std::ws. De lo contrario, si necesita borrar una entrada no válida independientemente de cuál sea, utilice ignore(). En nuestro ejemplo, solo necesitamos borrar los espacios en blanco ya que la transmisión consumió su entrada de "John"para la namevariable. Todo lo que quedaba era el carácter de nueva línea.


1: std::skipwses un manipulador que le dice al flujo de entrada que descarte los espacios en blanco iniciales al realizar una entrada formateada. Esto se puede apagar con el std::noskipwsmanipulador.

2: Los flujos de entrada consideran ciertos caracteres como espacios en blanco de forma predeterminada, como el carácter de espacio, el carácter de nueva línea, el avance de página, el retorno de carro, etc.

3: Esta es la firma de std::basic_istream<...>::ignore(). Puede llamarlo con cero argumentos para descartar un solo carácter de la secuencia, un argumento para descartar una cierta cantidad de caracteres o dos argumentos para descartar countcaracteres o hasta que llegue delim, lo que ocurra primero. Normalmente lo usa std::numeric_limits<std::streamsize>::max()como valor de countsi no sabe cuántos caracteres hay antes del delimitador, pero desea descartarlos de todos modos.

0x499602D2
fuente
1
¿Por qué no simplemente if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
@FredLarson Buen punto. Aunque no funcionaría si la primera extracción es de un número entero o cualquier cosa que no sea una cadena.
0x499602D2
Por supuesto, ese no es el caso aquí y no tiene sentido hacer lo mismo de dos maneras diferentes. Para un número entero, podría convertir la línea en una cadena y luego usarla std::stoi(), pero no está tan claro que haya una ventaja. Pero tiendo a preferir usar solo std::getline()para entrada orientada a líneas y luego tratar con analizar la línea de la manera que tenga sentido. Creo que es menos propenso a errores.
Fred Larson
@FredLarson estuvo de acuerdo. Tal vez agregue eso si tengo tiempo.
0x499602D2
1
@Albin La razón por la que podría querer usar std::getline()es si desea capturar todos los caracteres hasta un delimitador dado e ingresarlo en una cadena, por defecto es la nueva línea. Si ese Xnúmero de cadenas son solo palabras / tokens, entonces este trabajo se puede realizar fácilmente con >>. De lo contrario, ingresaría el primer número en un entero con >>, llamaría cin.ignore()en la siguiente línea y luego ejecutaría un bucle donde use getline().
0x499602D2
11

Todo estará bien si cambia su código inicial de la siguiente manera:

if ((cin >> name).get() && std::getline(cin, state))
Boris
fuente
3
Gracias. Esto también funcionará porque get()consume el siguiente carácter. También hay (std::cin >> name).ignore()lo que sugerí anteriormente en mi respuesta.
0x499602D2
"... trabajar porque get () ..." Sí, exactamente. Perdón por dar la respuesta sin detalles.
Boris
4
¿Por qué no simplemente if (getline(std::cin, name) && getline(std::cin, state))?
Fred Larson
0

Esto sucede porque un avance de línea implícito también conocido como carácter de nueva línea \nse agrega a todas las entradas del usuario desde un terminal, ya que le dice a la secuencia que comience una nueva línea. Puede tener esto en cuenta de forma segura utilizando std::getlinecuando compruebe varias líneas de entrada de usuario. El comportamiento predeterminado de std::getlineleerá todo hasta e incluido el carácter de nueva línea \ndel objeto de flujo de entrada que es std::cinen este caso.

#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
fuente