Почему std:: getline() пропускает ввод после форматированного извлечения?

У меня есть следующий фрагмент кода, который запрашивает у пользователя их имя и состояние:

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

Я нахожу, что имя успешно извлечено, но не состояние. Вот вход и результат:

Input:

"John"
"New Hampshire"

Output:

"Your name is John and you live in "

Почему имя состояния было исключено из вывода? Я дал правильный ввод, но код каким-то образом игнорирует его. Почему это происходит?

Ответ 1

Почему это происходит?

Это не имеет ничего общего с вводом, который вы предоставили сами, а скорее с поведением по умолчанию std::getline(). Когда вы указали свой ввод для имени (std::cin >> name), вы не только отправили следующие символы, но и неявный символ новой строки был добавлен в поток:

"John\n"

Новая строка всегда добавляется к вашему вводу, когда вы выбираете Enter или Return при отправке с терминала. Он также используется в файлах для перехода к следующей строке. Новая строка остается в буфере после извлечения в name до следующей операции ввода-вывода, где она либо сбрасывается, либо используется. Когда поток управления достигает std::getline(), новая строка будет отброшена, но ввод немедленно прекратится. Причина этого заключается в том, что функциональность по умолчанию этой функции диктует, что она должна (она пытается прочитать строку и останавливается, когда находит новую строку).

Поскольку этот ведущий символ новой строки запрещает ожидаемую функциональность вашей программы, из этого следует, что ее нужно как-то игнорировать. Один из вариантов - вызвать std::cin.ignore() после первого извлечения. Он будет отбрасывать следующий доступный символ, чтобы новая строка больше не мешала.

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

Подробное объяснение:

Это перегрузка std::getline(), которую вы вызвали:

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

Другая перегрузка этой функции принимает разделитель типа charT. Символ разделителя - это символ, представляющий границу между последовательностями ввода. Эта конкретная перегрузка по умолчанию устанавливает разделитель на символ новой строки input.widen('\n'), поскольку он не был указан.

Вот несколько условий, при которых std::getline() прекращает ввод:

  • Если поток извлек максимальное количество символов, которое может содержать std::basic_string<charT>
  • Если был найден символ конца файла (EOF)
  • Если разделитель был найден

Третье условие - это то, с которым мы имеем дело. Ваш вклад в state представлен следующим образом:

"John\nNew Hampshire"
     ^
     |
 next_pointer

где next_pointer - следующий символ для анализа. Поскольку символ, сохраненный в следующей позиции входной последовательности, является разделителем, std::getline() тихо отбросит этот символ, увеличит next_pointer до следующего доступного символа и остановит ввод. Это означает, что остальные предоставленные вами символы все еще остаются в буфере для следующей операции ввода-вывода. Вы заметите, что если вы выполните еще одно чтение из строки в state, ваше извлечение даст правильный результат, так как последний вызов std::getline() отбросил разделитель.


Возможно, вы заметили, что обычно вы не сталкиваетесь с этой проблемой при извлечении с помощью оператора форматированного ввода (operator>>()). Это связано с тем, что входные потоки используют пробелы в качестве разделителей для ввода, и манипулятор std::skipws 1 включен по умолчанию. Потоки будут отбрасывать начальные пробелы из потока, когда начнут выполнять форматированный ввод. 2

В отличие от форматированных операторов ввода, std::getline() является неформатированной функцией ввода. И все неотформатированные функции ввода имеют следующий общий код:

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

Выше представлен часовой объект, который создается во всех отформатированных/неформатированных функциях ввода-вывода в стандартной реализации C++. Объекты Sentry используются для подготовки потока к вводу-выводу и определения того, находится ли он в состоянии сбоя. Вы найдете только то, что в неотформатированных функциях ввода вторым аргументом конструктору sentry является true. Этот аргумент означает, что начальные пробелы не будут отбрасываться с начала входной последовательности. Вот соответствующая цитата из Стандарта [§27.7.2.1.3/2]:

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

[...] Если noskipws равен нулю, а is.flags() & ios_base::skipws не равен нулю, функция извлекает и отбрасывает каждый символ, пока следующий доступный входной символ c является символом пробела. [...]

Поскольку вышеприведенное условие ложно, сторожевой объект не будет отбрасывать пробел. Причина, по которой noskipws устанавливается на true с помощью этой функции, заключается в том, что цель std::getline() заключается в чтении необработанных неформатированных символов в объект std::basic_string<charT>.


Решение:

Нет способа остановить это поведение std::getline(). Что вам нужно сделать, так это сбросить новую строку самостоятельно до запуска std::getline() (но сделайте это после форматированного извлечения). Это можно сделать с помощью ignore(), чтобы отбросить оставшуюся часть ввода, пока мы не достигнем новой новой строки:

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

Вам нужно будет включить <limits>, чтобы использовать std::numeric_limits. std::basic_istream<...>::ignore() - это функция, которая отбрасывает определенное количество символов, пока не найдет разделитель или не достигнет конца потока (ignore() также отбрасывает разделитель, если он его находит). Функция max() возвращает наибольшее количество символов, которое может принять поток.

Другой способ отбросить пробел - использовать функцию std::ws, которая является манипулятором, предназначенным для извлечения и отбрасывания начальных пробелов из начала входного потока:

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

Какая разница?

Разница в том, что ignore(std::streamsize count = 1, int_type delim = Traits::eof()) 3 без разбора отбрасывает символы до тех пор, пока он либо не сбросит символы count, не найдет разделитель (заданный вторым аргументом delim), либо не достигнет конца потока. std::ws используется только для удаления пробельных символов в начале потока.

Если вы смешиваете форматированный ввод с неформатированным вводом и вам нужно удалить остаточный пробел, используйте std::ws. В противном случае, если вам нужно удалить неверный ввод независимо от того, что это, используйте ignore(). В нашем примере нам нужно только очистить пробел, так как поток потребляет ваш ввод "John" для переменной name. Осталось только символ новой строки.


1: std::skipws - это манипулятор, который сообщает входному потоку отбрасывать начальные пробелы при выполнении форматированного ввода. Это можно отключить с помощью манипулятора std::noskipws.

2. Входные потоки по умолчанию считают определенные символы пробелами, такими как пробел, символ новой строки, перевод формы, возврат каретки и т.д.

3: Это подпись std::basic_istream<...>::ignore(). Вы можете вызвать его с нулевыми аргументами, чтобы отбросить один символ из потока, одним аргументом, чтобы отбросить определенное количество символов, или двумя аргументами, чтобы отбросить символы count, или пока он не достигнет delim, в зависимости от того, какой из них появится первым. Обычно вы используете std::numeric_limits<std::streamsize>::max() в качестве значения count, если вы не знаете, сколько символов перед разделителем, но вы все равно хотите их отбросить.

Ответ 2

Все будет ОК, если вы измените исходный код следующим образом:

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

Ответ 3

Это происходит потому, что неявный перевод строки, также известный как символ новой строки \n, добавляется ко всем пользовательским вводам с терминала, когда он сообщает потоку начать новую строку. Вы можете смело учитывать это, используя std::getline при проверке нескольких строк пользовательского ввода. Поведение по умолчанию для std::getline будет считывать все, вплоть до символа новой строки \n из объекта входного потока, в данном случае это std::cin.

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