可以在以下链接中找到此实用程序的更合适版本:在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没问题吗?这是一个好习惯吗?
还有其他明显的错误吗?


评论

std :: getline(std :: cin,string)?

@Soapy是的,但是没有提示选项。

#1 楼

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的要点是将内容分开。因此,将您自己的东西放入自己的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
How old are you? 17
Hello, 17 year old!


实际操作中:

$ ./a.out
How old are you? don't care
Hello, 0 year old!
$


不用担心,如果不能复制上面两个示例的结果,第二个示例的结果。这是不确定的行为,因此任何事情(包括但不限于猫怀孕)都可能发生。

这是什么原因?

有两个问题。首先,在

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


如果TInput是类似int的内置类型,则不会在行1上初始化变量user_input_value。如果行3上的输入成功,则将该值设置为输入,就可以了。但是,如果输入无效,则不会分配任何值,并且您将返回未初始化的值。如果流是good(),则第3行上的提取运算符将成功提取并分配值,或者如果给出了无效输入,则设置failbit并分配0。因此,如果第一个输入无效,则将返回0(对于ounces)和failbit集。然后,在第二个条目上,未为user_input_value分配任何内容,并返回未初始化的int(对于age)。这将导致不确定的行为。

通过类似Valgrind的工具再次运行上述示例(“啤酒太多盎司”)示例,可以揭示错误。 (令我惊讶的是,ASan和UbSan都无法检测到该错误。)


我本来以为如果输入由于某种原因而失败,则目标值将永远不会改变。似乎是这种情况,但显然在C ++ 11中进行了更改,因此现在将0分配给无效输入,前提是该流以good()开头。感谢Mooing Duck的发现(请参阅注释)。


这个问题看似简单的解决方法是对user_input_variable使用值初始化。 br />
由于您的问题已用C ++ 14标记,因此我们现在至少可以自豪地使用一个C ++ 11功能(统一初始化)。

但是,“解决方案”解决了未定义的行为,但仍然存在问题。如果用户输入了无效的输入,则可能会大吼大叫,而不返回0(或任何默认构造的TInput)而无提示地返回。因此,您应该在操作后真正检查流。

$ ./a.out
How many ounces of beer dou you want? too many
How old are you? Here are your 0 ounces of beer.
$


您可以通过设置exceptionsstd::stdin掩码来达到相同的效果,但是这也会影响std::cin的其他用途,甚至在您的功能之外,因此可能会使您的功能用户感到惊讶。我认为实用程序功能会弄乱掩码。

有些人会认为用户输入无效数据绝不是“例外”事件,因此抛出异常是不合适的。如果您喜欢它们,则可能希望返回一个std::experimental::optional<TInput>结果。不幸的是,它还不是标准的,但是许多实现都支持它,而Boost.Optional中有一个易于使用的版本。

但是它仍然没有像它可能有用的那样。再次考虑“啤酒”示例。

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


函数的第一次调用愉快地消耗了4,在空白处停止并将2留在流中。然后,第二次调用将看到2随时可用,而无需等待任何用户输入,立即且均等地返回2。

cr_oag建议您通过调用std::cin.ignore来解决此问题。但是,我认为这不是理想的解决方案。如果您问用户她想要多少盎司的啤酒并输入4 2,那么她不太可能真正表示4。最好将其视为错误并要求澄清。

有当前版本的另一个密切相关的问题。考虑一下这个看似合理的程序是如何实现的...

TInput user_input_value {};


…表现不如预期。

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


可能会产生一种温暖舒适的感觉,以该程序的名字进行调用,我们大多数人可能会认为这是一个错误。

毫不奇怪,我建议您始终将面向用户输入的内容视为行:读取一行但完整的行,并将其整体视为一个输入。您可以通过std::getline轻松完成此操作。然后,您可以尝试将该字符串转换为目标类型,并查看其是否还有任何字符。

$ ./a.out
How many ounces of beer dou you want? 4 2
How old are you? Sorry, you're not old enough.
$


这还是不理想的。尽管它的行为符合预期,例如数字,但要求输入名称的示例现在将拒绝任何超过一个单词的输入。不完全是我们想要的。您将需要一些模板元编程来输入特殊情况的字符串。但是,要解释如何工作,要在代码审查中花费太多的时间。

事实上,Boost.Lexical_Cast中已经有一个函数模板可以为您执行此操作(以及更多操作) (#include <boost/lexical_cast.hpp>)。

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


可移植性

user86418已经提到#pragma once不可移植,但是如果使用它,则#include保护罩将变得多余。另一方面,#pragma once#include保护器效率更高,因为在使用#include保护器的情况下,预处理器可以立即停止处理文件,无论如何,它必须处理整个标头以找出匹配的#endif的位置。因此,如果您希望两全其美,则可以使用类似的方法。

$ ./a.out
What's your name? Ethan Bierlein
Hello, Ethan!
$


然后,您的用户可以使用-DHAVE_PRAGMA_ONCE进行编译,以最终加快编译速度,同时仍然可以可移植到没有#pragma once的实现中。如果您习惯使用GNU Autoconf,您将非常熟悉HAVE_${FEATURE}宏。

另一个选择是使用诸如

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;


(GCC定义了__GNUC__),但是我不太喜欢这种聪明,更喜欢让用户拥有硬道理。

正如Mooing Duck指出的那样,编译器应忽略未知的#pragma。但是,使其成为条件对象仍然是一个好主意。例如,如果您使用-Werror=unknown-pragmas进行编译(并且应该进行编译,因为它已由-Wall -Werror启用),则GCC将拒绝带有未知#pragma的代码。这与标准配置一致,GCC会按理应优雅地忽略它们。您的资料库不应强迫用户使用不太严格的警告级别。至少可以这样说,这样的“嘈杂”或“不洁”库非常令人讨厌。

泛型

其他人建议使流从参数读取或接受其他prompt比字符串。我认为这不会增加太多价值,因为我还没有看到一个程序需要从标准输入之外的任何内容读取交互式用户输入。不过,有些迷失的灵魂可能想阅读std::wcin。那些可能还会对将流式传输到std::wstringprompt感兴趣。最终,可能有充分的理由将std::wcout打印到标准错误输出(例如,如果要将标准输出重定向到某个文件)。

具有不使用(或默认)提示的重载似乎有用,但我认为没有其他必要。默认参数在这里很方便。

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);


是的,如果您使用字符串文字调用函数,您将不必要地构造一个临时prompt,但是(请参见下一个)部分)?而且我不认为将std::string用作int会是一个非常频繁的用例。

性能


有什么可以可以提高性能吗?


也许可以,但是不用担心。您正在要求用户输入。输入函数将花费非常糟糕的代码,与人类需要阅读,思考和键入的时间相比,输入函数的速度足够慢,无法产生任何差异。如果您想让程序看起来不错,请考虑添加行编辑支持。对于用户而言,这非常烦人,仅因为您意识到前几个字符有错字,才必须删除几乎完全键入的答案。 GNU Readline库为此提供了事实上的标准机制。使用GNU Readline,还有GNU历史记录,它允许用户使用箭头键重新获取以前的答案。如果您必须多次给出相同或相似的答案,这非常方便。当然,您应该使该功能为可选功能,因为您的用户可能尚未安装该库。

还要注意许可证要求;请注意。引用项目网站:


Readline是免费软件,根据GNU通用公共许可版本3分发。这意味着,如果您想在程序中使用Readline,发布或分发给任何人,该程序必须是免费软件并具有GPL兼容许可证。


如果您根本不发布软件,也可以。因此,您始终可以自由地进行实验,以进行个人学习和私人使用。

可能还有其他图书馆提供类似的行编辑支持,但我不知道其中的任何一个。

威廉·托特兰(Williham Totland)表示,还有另一个用于行编辑的免费软件Linenoise。在检查其网站时,我还找到了Editline库(libedit)(也是免费软件)。我从来没有使用过它们。除了您不应该在prompt中声明东西外,还把DocString和实现一起放置了。如果决定分开声明和定义,则DocString应该与声明一起使用,因为这对您的用户很重要。

我假设您想编写一个可由Doxygen处理的DocString。如果是这样,请注意您应该使用namespace std记录模板参数。

评论


\ $ \ begingroup \ $
哇!这是一个非常彻底的答案。谢谢!
\ $ \ endgroup \ $
– Ethan Bierlein
2015年10月9日15:17

\ $ \ begingroup \ $
@EthanBierlein它的时间比预期的要长。很高兴您发现它有用。
\ $ \ endgroup \ $
– 5gon12eder
2015年10月9日15:21

\ $ \ begingroup \ $
我喜欢这就像一段40段的代码回顾,或多或少归结为“只是不做”。
\ $ \ endgroup \ $
–巴里
2015年10月9日15:25

\ $ \ begingroup \ $
IIRC,#pragma曾经不需要被保护,因为C规范指出未知的pragma应该被忽略。而且,几乎每个编译器都具有它。
\ $ \ endgroup \ $
–鸭鸭
2015年10月10日,0:39

\ $ \ begingroup \ $
该答案已被选为2015年最佳代码审查-最佳新人(答案)类别的获奖者。恭喜你!
\ $ \ endgroup \ $
– 200_success
16年1月16日,0:45

#2 楼

命名空间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;
}


提示或不提示

我想您也可以在不提示的情况下提供重载:

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


静态错误检查

这要求TInput是默认可构造的,因此我们应该对用户友好并断言:

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


运行时错误检查

如果输入流式传输失败怎么办?我们如何表明呢?也许我们只是不这样做而将其保留为“简单”。

评论


\ $ \ begingroup \ $
提示后的双与号(&&)是什么意思?
\ $ \ endgroup \ $
– Ethan Bierlein
2015年10月9日在2:43

\ $ \ begingroup \ $
@EthanBierlein在这种情况下,这是一个转发参考。它将匹配左值或右值。
\ $ \ endgroup \ $
–巴里
2015年10月9日在2:47

\ $ \ begingroup \ $
我认为流应该始终是第一个参数。这是一种惯例,似乎已在所有编程语言中普遍使用,因此恕我直言,坏掉它而不需要它是一个坏主意。
\ $ \ endgroup \ $
– Celtschk
15-10-10在15:28

#3 楼

过去我实际上已经实现了相同的功能,所以这里是我的评论。

行为错误?

这取决于您认为错误的地方。考虑运行您自己的示例并输入a a。第二个a将保留在流缓冲区中,并将自动分配给第二个变量。这显然会引起奇怪的行为,但与通常使用std::cin时发生的行为相同。函数。

更好的接口

为了提供更好的接口,您可以改为从流中读取单个值,然后丢弃流缓冲区中剩余的任何其他值;对easy_input<T>()的调用将始终返回一个单个值,并且连续调用将不会被强制采用流缓冲区中剩余的内容。

示例

/>特殊类型的专业化

您还可以提供一种通过模板专业化或通过其他功能来读取整行的方法,因为这对字符串(例如,读取短语)很有用。 br />
更多功能

您可以提供一种基于谓词获取值的方法。这样人们可以更容易地获得有效的价值。例如,我想从用户轻松输入0到10之间的整数。

示例

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]范围内的整数:

int int_between_0_and_10 = get<int>( [] ( int i ) { return i >= 0 && i <= 10; } );

您可以通过提供使用“错误隐藏模式”的函数来对此进行扩展”。也就是说,它们返回一个bool来指示输入操作是否成功,而结果存储在引用参数中。

其他

std::cin并不是唯一可以从中流式传输的对象。例如,您还可以从文件流式传输。您应该为函数的用户提供一种方法,以指定他们要从中流式传输的内容(这可以像具有std::istream&参数一样简单。

#4 楼


是否需要包括防护罩?


是的,但是#pragma once是多余的,而且也不便于携带。


可以吗?将easy_input修补到std中而不会出现问题?这是一个好习惯吗?


不要在namespace std;中放东西。除少数情况外,这通常是未定义的行为,但即使在这种情况下,也没有必要。


还有什么明显的错误吗?


是的,您的“ easy_input”实际上并没有使任何事情变得容易。只是比较冗长。我可以看到的这种琐碎代码的唯一改进是添加了错误检查功能:

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


仅供参考,代码中没有与C ++ 14或模板元远程相关的内容编程。

#5 楼

首先,将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
    }
}