Легче вводить пользователя в C ++

Более подходящую версию этой утилиты можно найти по следующей ссылке: Предоставляет мне более легкий пользовательский ввод в C ++ - следить .


Меня всегда беспокоил тот факт, что для того, чтобы получить пользовательский ввод на C ++, нужно было использовать до трех строк кода уродливо, как показано ниже, чтобы получить пользовательский ввод для определенного типа , с подсказкой:

int user_input;
std::cout << ">> ";
std::cin << user_input;

Итак, чтобы упростить этот процесс, я создал функцию easy_input, которая позволяет пользователю указывать тип, приглашение и вводить пользовательский ввод переменной, все в одна строка.

easy_input.h

#ifndef EASY_INPUT_H_
#define EASY_INPUT_H_
#pragma once

#include <iostream>
#include <string>

// We're simply "re-defining" the standard namespace
// here so we can patch our easy_input function into
// it for the user's sake.
namespace std
{
    template <typename TInput>
    TInput easy_input(const std::string& prompt);
}

/**
 * This function serves as a wrapper for obtaining
 * user input. Instead of writing three lines every
 * time the user wants to get input, they can just
 * write one line.
 * @param {any}    TInput - The type of the input.
 * @param {string} prompt - The prompt to use for input.
 * @returns - Whatever the user entered.
 */
template <typename TInput>
TInput std::easy_input(const std::string& prompt)
{
    TInput user_input_value;
    std::cout << prompt;
    std::cin >> user_input_value;
    return user_input_value;
}

#endif

main.cpp (тесты)

#include <iostream>
#include <string>
#include "easy_input.h"

int main()
{
    const std::string user_input1 = std::easy_input<std::string>(">> ");
    std::cout << user_input1 << "\n";

    const int user_input2 = std::easy_input<int>(">> ");
    const int user_input3 = std::easy_input<int>(">> ");
    std::cout << user_input2 + user_input3 << "\n";
}

Я бы (желательно) хотел бы знать следующие вещи:

  • Я использую шаблоны соответствующим образом? Я чувствую, что, возможно, я сделал что-то неправильно в этом процессе.
  • Есть ли что-то, что можно улучшить с точки зрения производительности?
  • Есть ли необходимость включить охранников?
  • Можно ли без проблем исправить easy_input в std? Это хорошая практика?
  • Есть ли что-то еще, что нелепое?
38 голосов | спросил Ethan Bierlein 9 +03002015-10-09T05:01:00+03:00312015bEurope/MoscowFri, 09 Oct 2015 05:01:00 +0300 2015, 05:01:00

5 ответов


63

namespace std

Другие уже сказали это, но достаточно важно повторить: Не ставьте свои собственные определения в namespace std. Это неопределенное поведение.

О том, что я знаю, что вы можете помещать в namespace std, являются специализации для шаблонов, уже определенных стандартным . Так, например, если у вас

struct MyType
{
  int a;
};

inline constexpr bool
operator==(const MyType& lhs, const MyType& rhs) noexcept
{
  return lhs.a == rhs.a;
}

, то вы можете сделать

#include <functional>  // std::hash

namespace std
{

  template <>
  struct hash<MyType>
  {
    using argument_type = MyType;
    using result_type = std::size_t;

    result_type
    operator()(const argument_type& mt) const noexcept
    {
      return mt.a;
    }
  };

}

, чтобы вы могли, например, использовать MyType как ключ в std::unordered_map.

Вся суть пространства namespace s - это разделить материал. Поэтому добавьте свои собственные материалы в свое собственное namespace. Например, Boost имеет свой материал в namespace boost и под namespace. Вы можете использовать namespace ethan_bierlein или что-то еще. На других языках общепринятой практикой является использование своего домена, как в пакете package com.example.myproduct (при условии, что вы владеете example.com), но я не видел эту практику в C ++.

Корректность

Рассмотрим следующую программу.

int
main()
{
  const auto age = easy_input<int>("How old are you? ");
  std::cout << "Hello, " << age << " year old!\n";
}

Если скомпилировано и запущено как это

$ ./a.out
Сколько тебе лет?  17 
Привет, 17 лет!

все кажется прекрасным. Однако, если пользователь вводит бессмыслицу,

$  ./a.out 
Сколько тебе лет?  не заботится 
Привет, 0-летний!
$

Результат, вероятно, не тот, который вы ожидаете от хорошо продуманного пользовательского интерфейса. Тем не менее, это ухудшается.

int
main()
{
  const auto ounces = easy_input<int>("How many ounces of beer dou you want? ");
  const auto age = easy_input<int>("How old are you? ");
  //std::cout << "ounces = " << ounces << ", age = " << age << "\n";
  if (age >= 18)
    std::cout << "Here are your " << ounces << " ounces of beer.\n";
  else
    std::cout << "Sorry, you're not old enough.\n";
}

В действии:

$  ./a.out 
Сколько унций пива вы хотите?  слишком много 
Сколько тебе лет? Вот ваши 0 унций пива.
$

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

В чем причина этого?

Есть две проблемы. Во-первых, в

TInput user_input_value;        // (1)
std::cout << prompt;            // (2)
std::cin >> user_input_value;   // (3)
return user_input_value;        // (4)

if TInput является встроенным типом типа int, переменная user_input_value не инициализируется в строке 1. Если вход в строка 3 преуспевает, значение устанавливается на вход, что отлично. Однако, если вход недействителен, никакое значение не будет назначено, и вы вернете неинициализированное значение. Если поток good() , оператор извлечения в строке 3 либо успешно извлечет, либо присвоит значение, либо, если указан неверный ввод, установите failbit и назначить 0. Следовательно, если первый вход недействителен, то будет возвращено 0 (для ounces) и failbit. Затем во второй записи ничего не присваивается user_input_value и возвращен неинициализированный int (для age)). Это приводит к неопределенному поведению.

Запуск приведенных выше примеров second (например, множество унций пива) с помощью инструмента, такого как Valgrind может сообщить об ошибке. (К моему удивлению, ни ASan , ни UbSan смогли обнаружить ошибку.)

  

Первоначально я думал, что если вход по какой-либо причине невозможен, значение назначения никогда не будет изменено. Это похоже на случай, но , по-видимому, был изменен в C ++ 11 , так что теперь 0 назначается дляневерный ввод, если поток был good() для начала. Благодаря Mooing Duck для обнаружения этого (см. Комментарии).

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

TInput user_input_value {};

Поскольку ваш вопрос помечен C ++ 14, мы можем по крайней мере использовать одну функцию C ++ 11 (равномерную инициализацию) с гордостью.

Однако, хотя это «решение» устраняет неопределенное поведение, оно все еще имеет свои проблемы. Если пользователь вводит недопустимый ввод, он, вероятно, должен быть закричен, а не 0 (или какой-либо построенный по умолчанию TInput) возвращается молча. Поэтому вы должны действительно проверить поток после операции.

TInput user_input_value {};  // value-initialization not strictly needed any more
std::cout << prompt;
if (std::cin >> user_input_value)
  return user_input_value;
throw std::istream::failure {"bad user input"};

Вы можете получить такой же эффект, установив exceptions маска std::stdin, но это также повлияет на другие применения std::cin даже вне вашей функции, чтобы она могла удивить пользователей вашей функции. Я бы подумал, что функция утилиты messing с маской плохая.

Некоторые люди будут утверждать, что пользователь, вводящий недопустимые данные, никоим образом не является «исключительным» событием, поэтому исключение исключения является неуместным. Если вы похожи на них, вы можете предпочесть вернуться к std::experimental::optional<TInput> . К сожалению, это еще не стандарт, но многие реализации поддерживают его, и есть доступная версия в Boost.Optional .

Но это все еще не так полезно, как могло бы быть. Снова рассмотрите пример «beer».

$  ./a.out 
Сколько унций пива вы хотите?  4 2 
Сколько тебе лет? Извините, вы недостаточно взрослые.
$

Первый вызов вашей функции с удовольствием потребляет 4, останавливается в белом пространстве и оставляет 2 в потоке. Второй вызов затем видит 2 с готовностью доступным и не дожидаясь ввода пользователем, сразу и равно счастливо возвращает 2.

cr_oag предложил , что вы обращаетесь к этому, вызывая std::cin.ignore . Однако я не думаю, что это идеальное решение. Если вы спросите у пользователя, сколько унций пива она хочет, и она вводит 4 2, маловероятно, что она действительно имела в виду 4. Лучше было бы рассматривать это как ошибку и просить разъяснений.

Есть и другая, тесно связанная проблема с вашей текущей версией. Подумайте, как эта, казалось бы, разумная программа ...

int
main()
{
  const auto name = easy_input<std::string>("What's your name? ");
  std::cout << "Hello, " << name << "!\n";
}

â € | не так, как ожидалось.

$  ./a.out 
Как вас зовут?  Этан Бирлейн 
Привет, Итан!
$

В то время как это может создать теплое и уютное чувство, которое будет вызываться с первым именем программы, большинство из нас, вероятно, считают это ошибкой.

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

std::cout << prompt;
std::string line {};
if (!std::getline(std::cin, line))
  throw std::istream::failure {"I/O error"};
std::istringstream iss {line};
TInput value {};
if (!(iss >> value) || !iss.eof())
  throw std::istream::failure {"bad user input"};
return value;

Это все еще не идеально. Пока он ведет себя так, как ожидалось, например, числа, пример, запрашивающий имя, теперь будет отклонять любой ввод дольше одного слова. Не совсем то, что мы хотим. Вам понадобится мета-программирование шаблонов для ввода строки в специальный регистр. Но объяснить, как это будет работать, слишком много, чтобы пройти проверку кода.

На самом деле уже существует шаблон функции в Boost .Lexical_Cast , который делает это (и более) для вас (#include <boost/lexical_cast.hpp>)

std::string line {};
std::cout << prompt;
if (!std::getline(std::cin, line))
  throw std::istream::failure {"I/O error"};
return boost::lexical_cast<TInput>(line);

Портативность

user86418 уже упоминал , что #pragma once не переносится, но если вы его используете, #include охранники становятся излишними. С другой стороны, #pragma once может быть более эффективным, чем защита #include, поскольку препроцессор может немедленно прекратить обработку файла, используя #include он должен обработать весь заголовок, чтобы узнать, где находится соответствующий #endif. Поэтому, если вы хотите получить лучшее из обоих миров, вы можете использовать что-то вроде этого.

#if HAVE_PRAGMA_ONCE
#pragma once
#endif

#ifndef EASY_INPUT_H
#define EASY_INPUT_H

// ...

#endif

Затем ваши пользователи могли бы скомпилировать с помощью -DHAVE_PRAGMA_ONCE, чтобы в конечном итоге ускорить их компиляцию, будучи еще переносимым для реализаций, которые не имеют #pragma once. Если вы привыкли использовать GNU Autoconf , вы будете очень знакомы с макросами HAVE_${FEATURE}.

Другой вариант - использовать трюки типа

#ifdef __GNUC__
#pragma once
#endif

( GCC определяет __GNUC__ ), но я не «Мне очень нравится этот ум, и предпочитаю, чтобы у пользователя было последнее слово.

Как Mooing Duck указывает, компиляторы должны игнорировать неизвестный #pragma s. Тем не менее, это все же хорошая идея сделать их условными. Например, если вы компилируете с помощью -Werror=unknown-pragmas (И вы должны, потому что он включен с помощью -Wall -Werror.), GCC будет отклонять код с неизвестным #pragma s. Это соответствует, как в стандартной конфигурации, GCC игнорирует их изящно, как предполагается. Ваша библиотека не должна заставлять ваших пользователей использовать менее строгие уровни предупреждений. Такие â no no no no>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

Генеричность

Другие предложили сделать поток для чтения из параметра или принять другое приглашение prompt, чем строки. Я не думаю, что это добавило бы большой ценности, поскольку я еще не видел программу, которая должна читать интерактивный пользовательский ввод от всего, кроме стандартного ввода. Некоторые потерянные души могут захотеть прочитать std::wcin . Те, вероятно, также будут заинтересованы в std::wstring prompt, который будет передан в std::wcout . В конце концов, могут быть веские причины напечатать приглашение prompt для стандартного вывода ошибки (например, если стандартный вывод должен быть перенаправлен на некоторый файл).

Наличие перегрузки, которая не использует приглашения (или по умолчанию), кажется полезным, но все остальное, на мой взгляд, является излишним. Здесь полезен параметр по умолчанию.

template <typename TInput>
TInput easy_input(const std::string& prompt = "");

Да, вы будете бесполезно создавать временную std::string, если вы вызываете функцию с строковым литералом, но так что (см. следующий раздел)? И я не думаю, что использование, скажем, int в виде prompt было бы очень частым случаем использования.

Производительность

  

Есть ли что-то, что можно улучшить с точки зрения производительности?

Может быть, но не беспокойтесь об этом. Вы запрашиваете ввод . Для функции ввода, которая будет достаточно медленной, чтобы иметь какое-то значение по сравнению с тем, как долго люди должны читать, думать и печатать, потребуется very плохой код для функции ввода.

Going Fancy

Если вы хотите сделать свою программу интересной, подумайте над добавлением поддержки редактирования строк. Это очень неприятно для пользователя, поэтому нужно удалить почти полностью типизированный ответ только потому, что вы понимаете, что у вас есть опечатка в первых нескольких символах. Библиотека GNU Readline предоставляет де-факто стандартный механизм для этого. С помощью GNU Readline также появляется История GNU , которая позволяет пользователю повторно использовать предыдущий ответ с помощью клавиш со стрелками. Это очень удобно, если вы должны много раз давать один и тот же ответ или подобные ответы. Конечно, вы должны сделать эту опцию необязательной, потому что ваши пользователи могут неустановили библиотеку.

Также обратите внимание на требования к лицензии; цитируя веб-сайт проекта:

  

Readline - бесплатное программное обеспечение, распространяемое в соответствии с условиями GNU General Public License , версия 3. Это означает, что если вы хотите использовать Readline в программе, которую вы выпускать или распространять кого-либо, программа должна быть свободным программным обеспечением и иметь лицензию, совместимую с GPL.

Если вы вообще не публикуете свое программное обеспечение, вы тоже в порядке. Таким образом, вы всегда можете свободно экспериментировать для личного обучения и частного использования.

Возможно, существуют и другие библиотеки, предоставляющие аналогичную поддержку редактирования строк, но я не знаю ни одного из них.

Williham Totland прокомментировал, что есть еще одна библиотека бесплатного программного обеспечения для редактирования строк, Linenoise . При проверке своего веб-сайта я также нашел библиотеку редактирования (libedit) (также бесплатное программное обеспечение). Я никогда не использовал ни одного из них.

Стиль

Помимо того, что вы не должны объявлять материал в namespace std, вы добавили DocString вместе с реализацией. Если вы решите отделить объявление и определение, DocString должен пойти с объявлением, потому что это важно для ваших пользователей.

Я предполагаю, что вы хотели написать DocString, который может быть обработан Doxygen . Если да, обратите внимание, что для документирования аргументов шаблона следует использовать @tparam.

ответил 5gon12eder 9 +03002015-10-09T18:10:12+03:00312015bEurope/MoscowFri, 09 Oct 2015 18:10:12 +0300 2015, 18:10:12
11

namespace std

зарезервировано. Не добавляйте к нему ничего. Тем более, что в этом нет причин. Просто поставьте его в свое собственное пространство имен.

ненужное использование строк

На самом деле нет причин принимать приглашение в виде std::string. Вы могли бы сохранить его в качестве символьного литерала. Действительно, все, что можно потокоть, достаточно хорошо, так что это может быть просто:

template <typename TInput, typename Prompt>
TInput easy_input(Prompt&& prompt)
{
    TInput user_input_value;
    std::cout << std::forward<Prompt>(prompt);
    std::cin >> user_input_value;
    return user_input_value;
}

Чтобы сделать cin или нет cin

Что делать, если я хочу ввести откуда-нибудь еще? Предоставим эту возможность:

template <typename TInput, typename Prompt>
TInput easy_input(Prompt&& prompt, std::istream& is = std::cin)
{
    TInput user_input_value;
    std::cout << std::forward<Prompt>(prompt);
    is >> user_input_value;
    return user_input_value;
}

Чтобы запросить или не запросить

Я думаю, вы также можете обеспечить перегрузку с помощью no :

template <typename TInput>
TInput easy_input(std::istream& is = std::cin)
{
    TInput user_input_value;
    is >> user_input_value;
    return user_input_value;
}

template <typename TInput,
          typename Prompt,
          typename = decltype(std::cout << std::declval<Prompt>())>
TInput easy_input(Prompt&& prompt, std::istream& is = std::cin)
{
    std::cout << std::forward<Prompt>(prompt);
    return easy_input<TInput>(is);
}

статическая проверка ошибок

Для этого нужно, чтобы TInput был по умолчанию конструктивным, поэтому мы должны быть хороши для наших пользователей и утверждать, что:

static_assert(std::is_constructible<TInput>{}, "!");

проверка ошибок во время выполнения

Что делать, если поток ввода не работает? Как мы это обозначим? Возможно, мы просто не делаем этого и оставляем его «легким».

ответил Barry 9 +03002015-10-09T05:41:11+03:00312015bEurope/MoscowFri, 09 Oct 2015 05:41:11 +0300 2015, 05:41:11
11

Я действительно реализовал ту же функциональность в прошлом, так что вот мои комментарии.

Неверное поведение?

Это зависит от того, что вы можете считать неправильным. Попробуйте запустить собственный пример и введите a a

Предложения

Ниже перечислены то, что я считаю полезными для такой функции полезности.

Улучшенный интерфейс

Чтобы обеспечить более удобный интерфейс, вы могли бы вместо этого прочитать одно значение из своего потока и затем отбросить все остальное, оставшееся в буфере потока; вызовы easy_input<T>() всегда будут возвращать одно единственное значение, и последующие вызовы не будут вынуждены брать то, что осталось в буфере потока.

Пример

template <typename T>
T get( std::istream& is = std::cin )
{
    T value{};
    is >> value;
    is.ignore( std::numeric_limits<std::streamsize>::max(), '\n' );
    return value;
}

Специализация для специальных типов

Вы также можете указать способ чтения целых строк с помощью специализированной специализации или с помощью другой функции, так как это было бы полезно для строк (например, для чтения фразы).

Дополнительные функции

Вы можете предоставить способ получения значений на основе предиката. Так что людям может быть намного легче получить действительные значения. Например, я хочу получить легкий ввод целого числа от 0 до 10 от пользователя.

Пример

template <typename T, typename Predicate>
T get( Predicate pred, std::istream& is = std::cin )
{
    T value{};
    while ( !pred( value = get<T>() ) ) // uses regular get<T>()
    {}
    return value;
}

, который затем можно вызвать следующим образом, чтобы получить целое число в диапазоне [0, 10]:

>>

Вы можете расширить это, предоставив функции, которые используют «шаблон скрытия ошибки». Это значит, что они возвращают int int_between_0_and_10 = get<int>( [] ( int i ) { return i >= 0 && i <= 10; } );, чтобы указать, была ли операция ввода успешной или нет, тогда как результат сохраняется в контрольном параметре.

Другие

bool - это не единственный объект, из которого можно передавать потоки. Например, вы также можете передавать потоки из файлов. Вы должны предоставить пользователям возможность указать, с чего они хотят передать (это может быть так же просто, как наличие параметра std::cin.

ответил cr_oag 9 +03002015-10-09T05:35:58+03:00312015bEurope/MoscowFri, 09 Oct 2015 05:35:58 +0300 2015, 05:35:58
3
  

Есть ли необходимость включить охранников?

Да, но #pragma once является избыточным, а также не переносимым.

  

Можно ли легко вставлять easy_input в std без проблем? Это хорошая практика?

Не помещайте содержимое в пространство имен namespace std;. Обычно это неопределенное поведение за исключением нескольких случаев , но даже тогда оно не требуется удаленно в Это дело.

  

Есть ли что-нибудь еще, что очень плохо?

Да, ваш «easy_input» на самом деле не делает ничего проще. Это просто более подробный. Единственное улучшение для такого тривиального кода, которое я вижу, это добавить проверку ошибок:

if (!(std::cin >> user_input_value))
{
    // error
}

FYI нет ничего в коде, удаленно связанном с C ++ 14 или мета-программированием шаблонов.

ответил user86418 9 +03002015-10-09T05:06:35+03:00312015bEurope/MoscowFri, 09 Oct 2015 05:06:35 +0300 2015, 05:06:35
2

Сначала добавление пространства имен easy_input в std - UB. Единственными дополнениями, которые вы разрешите сделать в пространстве имен std, являются специализированные шаблоны для ваших собственных типов (то есть, если вы создаете тип, называемый MyClass), вы можете специализировать swap для него в пространстве имен std).

Во-вторых, вы жестко закодировали входные и выходные потоки; Вместо этого сделайте следующее:

namespace stdex // not std namespace
{

    /**
     * This function serves as a wrapper for obtaining
     * user input. Instead of writing three lines every
     * time the user wants to get input, they can just
     * write one line.
     * @param {any}    Input - The type of the input.
     * @param {any}    Prompt - The prompt to use for input.
     * @returns - Whatever the user entered.
     * @throws std::runtime_error on bad input
     */
    template <typename Input, typename Prompt>
    Input easy_input(const Prompt& prompt,
        std::istream& in = std::cin)
    {
        auto user_input_value = Input{}; // initialize value on creation
        auto tied_stream = in.tie();
        if(tied_stream)
            (*tied_stream) << prompt;
        if(!(in >> user_input_value))
            throw std::runtime_error{ "easy_input: Bad input" }; // handle errors
        return user_input_value;
    }
}

Клиентский код:

int main()
{
    auto a = stdex::easy_input<std::string>("name: ");

    std::istringstream in{ "aaa bbb" };
    auto b = stdex::easy_input<std::string>(">> ", in); // will not print a prompt (in is not
                                                        // tied to any output stream)

    std::ostringstream out;
    in.tie(&out);
    auto c = stdex::easy_input<std::string>(">> ", in);
    assert(">> " == out.str());

    try
    {
        auto d = stdex::easy_input<int>(">> ", in);
    }
    catch(const std::runtime_error&)
    {
        // code will get here because in is at EOS
    }
}
ответил utnapistim 9 +03002015-10-09T16:55:24+03:00312015bEurope/MoscowFri, 09 Oct 2015 16:55:24 +0300 2015, 16:55:24

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132