概述

在学习 C/C++ 的一开始,我们就接触到基本的输入输出,例如printf/scanf或者cout/cin, 但是在涉及用户输入时总是存在可靠性问题:我们无法确保用户输入的信息是合法的,比如要求输入一个数字,但是用户提供的是一串字符串等等,或者要求数字满足一定范围等,我们只能每次手动在外层加一个循环和判断条件,非常繁琐。 在本文中实现了一个简单的获取安全输入的封装SafeInput,使得在编程中尽可能摆脱这类涉及用户输入参数的处理细节。

使用示例

  1. 指定获取一个int类型的值,要求大于 0 且小于 5,如果输入不满足要求,则会提示重新输入
1
2
int s = SafeInput().get<int>("Please input a int in (0,5): ",
[](int p) { return 0 < p && p < 5; });
  1. 指定获取一个double类型的值,并且在获取值之后会回显这个值,需要用户继续输入Y确认,否则提示重新输入
1
double d = SafeInput().get<double>("Please input a double: ", true);
  1. 提示用户输入Y|N|Q进行确认,输入Y|N将通过布尔变量返回,输入Q则会使用exit(0)直接退出程序
1
bool s = SafeInput().confirm("Continue or exit?");
  1. 程序暂停并等待,直到获取到回车后,程序继续执行
1
SafeInput().pause();

使用要求

示例中的 1 和 2 体现了最主要的用法,其中输入的合法性检查和对输入的再次手动确认都是可选的,一共有四种组合,函数原型依次为

1
2
3
4
5
6
7
8
9
10
11
template <typename T, typename CheckerType>
T get(const std::string &msg, CheckerType checker,bool confirm) const;

template <typename T>
T get(const std::string &msg, bool confirm) const;

template <typename T, typename CheckerType>
T get(const std::string &msg, CheckerType checker) const;

template <typename T>
T get(const std::string &msg) const;

示例中的 3 和 4 则是作为常用情景被直接提供

1
2
3
4
5
bool confirm(const std::string &msg) const;

bool confirm() const;

void pause() const;

注意:

  • 建议使用 lambda 表达式提供合法性检查,要求返回布尔类型的检查结果,指定的输入类型T则通过函数模板提供。
  • 每一次的输入都处于循环之中,无论输入成功,还是失败再次输入,都会清空缓冲区,因此不支持连续的输入,这是出于可靠性的考虑。
  • 用于判断的特殊字符(Y|N|Q)均不区分大小写。
  • 默认会将所有的提示语输入到std::cout,并且从std::cin获取用户输入,但是也支持在构造函数中指定其它的输入和输出流。
1
2
3
4
5
SafeInput() : m_in(std::cin), m_out(std::cout) {}

explicit SafeInput(std::istream &in) : m_in(in), m_out(std::cout) {}

SafeInput(std::istream &in, std::ostream &out) : m_in(in), m_out(out) {}

完整的使用示例如下

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "safe_input.hpp"

int main(int argc, char *argv[]) {
auto s = SafeInput().get<int>("Please input a int in (0,5): ",
[](int p) { return 0 < p && p < 5; });
std::cout << "s=" << s << "\n";

auto d = SafeInput().get<double>("Please input a double: ", true);
std::cout << "final: d=" << d << "\n";

auto p = SafeInput().get<bool>("input a bool: ", true);
std::cout << "final: p=" << p << "\n";

SafeInput().confirm("Continue or exit?");

SafeInput().pause();

return 0;
}

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#pragma once

#include <iostream>
#include <limits>
#include <string>

class SafeInput {
public:
SafeInput() : m_in(std::cin), m_out(std::cout) {}

explicit SafeInput(std::istream &in) : m_in(in), m_out(std::cout) {}

SafeInput(std::istream &in, std::ostream &out) : m_in(in), m_out(out) {}

template <typename T, typename CheckerType>
T get(const std::string &msg, CheckerType checker, bool confirm) const {
return get_kernel<T>(msg, checker, confirm);
}

template <typename T>
T get(const std::string &msg, bool confirm) const {
return get_kernel<T>(msg, [](T &) { return true; }, confirm);
}

template <typename T, typename CheckerType>
T get(const std::string &msg, CheckerType checker) const {
return get_kernel<T>(msg, checker, false);
}

template <typename T>
T get(const std::string &msg) const {
return get_kernel<T>(msg, [](T &) { return true; }, false);
}

bool confirm(const std::string &msg) const {
bool status = false;
auto checker = [&](char s) {
if (is_char(s, 'Y', 'y')) {
status = true;
return true;
}
if (is_char(s, 'N', 'n')) {
status = false;
return true;
}
if (is_char(s, 'Q', 'q')) {
out("quit by SafeInput.");
exit(0);
}

return false;
};

get_kernel<char>(msg + " (Y|N|Q): ", checker, false);
return status;
}

bool confirm() const { return confirm("Confirm ?"); }

void pause() const {
out("Press enter to continue. ");
clear_buffer();
}

private:
std::istream &m_in;
std::ostream &m_out;

std::ostream &out(const std::string &msg) const { return m_out << msg; }

static bool is_char(char s, char upper, char lower) {
return (s == upper) || (s == lower);
}

void clear_buffer() const {
m_in.clear();
m_in.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

template <typename T>
bool in(T &receiver) const {
m_in >> receiver;
bool s = m_in.good();

clear_buffer();
return s;
}

template <typename T, typename CheckerType>
T get_kernel(const std::string &msg, CheckerType checker,
bool confirm) const {
check_type<T>();
auto confirmer = [&](char s) {
if (is_char(s, 'Y', 'y')) { return true; }
if (is_char(s, 'Q', 'q')) {
out("quit by SafeInput.");
exit(0);
}
return false;
};
T receiver;
char tmp = 0;

while (true) {
out(msg);

if (in(receiver) && checker(receiver)) { // pass from checking
if (!confirm) break; // without confirmation

out("Get: ") << receiver << '\n';
out("Confirm it? (Y|N|Q): ");

if (in(tmp) && confirmer(tmp)) break; // after confirmation
}

if (msg.empty()) out("Please input again.\n");
}
return receiver;
}

template <typename T1, typename T2>
static constexpr bool Is = std::is_same_v<T1, T2>;

template <typename T>
static void check_type() {
if constexpr (Is<T, std::string> || Is<T, bool> || Is<T, char>
|| Is<T, int> || Is<T, double> || Is<T, size_t>) {}
else { static_assert(Is<T, std::string>, "unsupported type"); }
}
};