第2.14章: A + B
基本的输入与输出
一个读取两个整数并打印它们之和的简单程序。它同时演示了用于输入的
scan 与用于输出的 println:
#include <fast_io.h>
#include <cstddef>
int main()
{
try
{
using namespace ::fast_io::iomnp;
::std::size_t a, b;
scan(a, b);
println(a + b);
}
catch throws (::std::error e)
{
::fast_io::perrln(e);
return 1;
}
}
就连这么小的程序也不能保证成功,而代码本身也表明了这一点:函数体放在
try 块中,catch throws 处理程序会报告失败
并返回非零退出码。下一节你将看到原因。
I/O 总是可能失败
这是本教程中第一个与外部世界打交道的程序,也正是从这一刻起,失败成为可能。 每一个 I/O 操作都可能失败。 不存在“安全”的 I/O 调用,也不存在某种配置能让你假定它一定成功。
输入随时都会失败。它失败的方式再普通不过:
-
用户输入了
x而不是数字,于是scan(a, b)没有可解析的内容——解析错误; -
输入在一个数字之后就结束了,于是第二次
scan读到文件结束; -
数字对
::std::size_t来说太大,于是溢出; - 底层设备返回错误——管道已关闭、连接被重置、磁盘坏扇区、 网络读取超时。
运行上面的程序并喂给它垃圾数据:
$ echo "x 4" | ./aplusb
[parse](3)Invalid format
$ echo $?
1
输出同样会失败。这让人吃惊,因为在
iostream 的世界里从没有人检查打印结果。但写入是对
操作系统(或网络对端)的请求,而请求是会被拒绝的:
-
磁盘满了,缓冲的字节无法冲刷
(
[posix](28)No space left on device); -
读取方消失了——流水线下游的进程死了,终端
被关闭(
[posix](32)Broken pipe); - 输出流是一个未以写入方式打开的文件句柄,或者指向一个 已满/只读的文件系统;
-
输出流是一个套接字,而对端重置了连接
(
[posix](104)Connection reset by peer)。我们稍后会接触套接字; 重要的是:连println("Hello World")都可能失败, 只要目的地不是本地控制台,而是网络对端、满磁盘上的文件, 或者一个已经退出的进程。
所以 println 并不是一条“只是打印”的语句。它是一个可能失败的操作,
而在 fast_io 中,每个可能失败的操作都会在它的类型中说明这一点。
试试看:
#include <fast_io.h>
using namespace ::fast_io::io;
// println can fail: it may have to write to a device
static_assert(!noexcept(println("Hello World")));
// scan can fail: it may have to read and parse
static_assert(!noexcept(scan(::fast_io::c_stdin(), ::std::declval<int&>())));
可能失败的调用会被声明为 throws。如果你调用了这样的函数,就必须
应对失败的可能性:要么用
try { } catch throws (::std::error e) { } 处理,要么把你自己的函数也标记为
throws,让失败传播给你的调用者。没有第三种选择
——编译器不允许你忽视它。
fast_io 中的错误以 赫布异常 的形式报告,它们
是本教程中唯一的错误机制。要构建它们,需要
-fherbceptions -lherbceptions,以及我们的某个 GCC 或 LLVM 构建(MSVC
不支持赫布异常)。完整的规则——选项、
throw throws 语句、处理程序,以及每一个错误域——都在
第3.16章: 赫布异常 中。从这里开始,
每一个执行 I/O 的示例都会使用你在上面看到的同一种 try { } catch throws
形态。
什么是操纵器?
在 fast_io 中,操纵器(manipulator)是一个辅助对象,用来改变
输入或输出被解释或格式化的方式。不必依赖不安全的格式字符串,
操纵器把意图显式地表达在类型系统中。
操纵器定义在命名空间 fast_io::manipulators 中,为方便起见别名为
fast_io::mnp。相比传统的 stdio 或 iostream,
它们让输入输出更安全、更显式、更可移植。
例如:
hex_get(a)—— 让scan以十六进制读取变量a。hex(a)—— 以小写十六进制打印a。hexupper(a)—— 以大写十六进制打印a。base_get<N>(a)—— 以 N 进制读取a(2 ≤ N ≤ 36)。base<N>(a)—— 以 N 进制打印a。
十六进制操纵器
fast_io 提供了以十六进制读写数字的操纵器。
hex_get 以十六进制读取输入,而 hex 与 hexupper
分别以小写或大写十六进制打印输出:
#include <fast_io.h>
#include <cstddef>
int main()
{
try
{
using namespace ::fast_io::iomnp;
::std::size_t a, b;
scan(hex_get(a), hex_get(b));
println(hex(a + b), " ", hexupper(a + b));
}
catch throws (::std::error e)
{
::fast_io::perrln(e);
return 1;
}
}
两个十六进制数进,两个十六进制数出。坏输入仍然终止于处理程序,而不会造成 未定义行为。
N 进制操纵器
操纵器也支持 2 到 36 的任意进制。下面是使用 36 进制的例子:
#include <fast_io.h>
#include <cstddef>
int main()
{
try
{
using namespace ::fast_io::iomnp;
::std::size_t a, b;
scan(base_get<36>(a), base_get<36>(b));
println(base<36>(a + b));
}
catch throws (::std::error e)
{
::fast_io::perrln(e);
return 1;
}
}
关于操纵器
fast_io 定义了一组丰富的操纵器来控制输入输出的格式化。
为方便起见,命名空间 fast_io::manipulators 别名为 fast_io::mnp。
base_get<N>—— 以 N 进制读取数字(其中 2 ≤ N ≤ 36)。base<N>—— 以 N 进制打印数字。hex——base<16>的简写。hexupper——base<16,true>的简写,以大写打印十六进制数字。left、right、internal、width、fill—— 字段布局与填充。
这些操纵器让你可以轻松处理不同的数字进制,而无需依赖不安全的格式字符串。
要点
- A + B 是执行 I/O 的最小程序,而 I/O 总是可能失败。
-
输入会因格式错误、文件结束、溢出和设备错误而失败;输出
会因磁盘满、管道破裂、句柄关闭和连接重置而失败。连
println("Hello World")都可能失败。 -
在
fast_io中,每个可能失败的操作都被声明为throws。 调用者要么用try { } catch throws (::std::error e) { }处理它, 要么自己也变成throws。 - 赫布异常是本教程中唯一的错误报告机制;完整规则见 第3.16章。
-
操纵器(
hex、base<N>、hex_get等) 无需格式字符串即可改变格式化方式,但它们并不能改变 周围那次 I/O 调用可能失败这一事实。