第26.2章:print

English

1. 函数表面

print 家族刻意保持很小。每个函数都返回 void,并通过 herbception 报告失败。 名字之间的差别只有:目标、换行,以及这个调用是否允许返回。

函数 默认目标 换行 可用性
print(args...) C stdout 否 始终
println(args...) C stdout 是,\n 始终
perr(args...) 原生 stderr 否 始终
perrln(args...) 原生 stderr 是,\n 始终
panic(args...) 原生 stderr 否 始终;会终止
panicln(args...) 原生 stderr 是,\n 始终;会终止
debug_print(args...) 原生 stdout 否 仅在未定义 NDEBUG 时
debug_println(args...) 原生 stdout 是 仅在未定义 NDEBUG 时
debug_perr(args...) 原生 stderr 否 仅在未定义 NDEBUG 时
debug_perrln(args...) 原生 stderr 是 仅在未定义 NDEBUG 时

每个函数也接受一个输出设备作为第一个参数: print(device, args...)、println(device, args...)、 perr(device, args...) 等等。第一个参数不是输出设备时,使用表里的默认目标。

perr 写平台原生的标准错误,而不是 C 的 stderr。 它不带缓冲,每次调用就是一次系统调用,这正好提供诊断所需的原子性。 panic 和 panicln 是同样的目标,然后执行 trap:它们不返回。

2. 第一个参数算什么

如果第一个参数满足 fast_io 使用的 output-stream 或 I/O-stream 概念,它就是设备; 否则它被当作值打印,并使用默认目标。所以下面两种写法都成立:


#include <fast_io.h>

int main()
{
    using namespace ::fast_io::io;

    // Value first: default destination.
    println("answer = ", 42);

    // Device first: explicit destination.
    ::fast_io::obuf_file out("out.txt");
    println(out, "answer = ", 42);
}

没有格式串,也没有每次调用的状态。设备只选一次,剩下的每个参数按自己的类型, 以及包在它外面的操纵器来格式化。这就是为什么 print 不需要 iostream 状态机也能组合:调用结束后没有东西需要恢复。

3. 格式化只作用于参数

操纵器是值,不是流标志。mnp::hex、mnp::fixed、 mnp::left 等等都只包住一个参数:


println("decimal: ", 255);
println("hex:     ", ::fast_io::mnp::hex(255));
println("padded:  ", ::fast_io::mnp::right(42, 8, '0'));

下一条语句不受影响。不存在忘记重置 std::hex 的等价问题, 也不存在操纵器泄漏到另一个流的等价问题。完整导出列表——包括进制、浮点、宽度、 指针、字符串、二进制、网络和 locale 操纵器——在 第26.1章。

自定义类型通过 print_reserve_define 或 print_define 加入同一条路径。print 函数不需要认识这个类型;它让类型的格式化器写进输出设备。 如果类型只能通过一段已有字节的 scatter 打印,就用 print_scatter_define。

4. 返回值与失败

print 家族没有返回值。失败以 herbception 抛出,所以通常的处理程序就是本书一直在用的那个:


#include <fast_io.h>

int main()
{
    try
    {
        ::fast_io::io::println(::fast_io::out(), "hello");
    }
    catch throws(::std::error e)
    {
        ::fast_io::io::perrln(e);
        return 1;
    }
}

给一个没有格式化器的类型做格式化是编译期错误,不是运行期错误。 运行期失败来自设备层:文件已关闭、磁盘满、管道破裂,或者系统调用被中断。

5. 缓冲、刷新与同步

print 对带缓冲和不带缓冲的输出设备都成立。带缓冲设备把小写操作攒起来, 缓冲区满或对象析构时刷出;不带缓冲设备直接写透。这就是 第11.5章讲过的划分, 也是 print 家族不强迫每个调用者选择缓冲区大小的原因。

perr 故意不带缓冲。即使进程即将 trap,或者另一个线程还持有带缓冲的流, 诊断也必须到达。代价是每次调用一次系统调用;不要在热循环里用 perr。

线程安全取决于设备。互斥包装的设备(例如可加锁文件类型)会把调用串行化。 普通带缓冲文件不会因为函数叫 println 就变成线程安全的。 进程安全是更强的性质:只有设备不是带缓冲输出流时,print 家族才保证它。

6. 调试输出

debug_* 家族的存在,是为了让诊断打印从发布构建里消失。 定义 NDEBUG 后这些函数不可用;仍然引用它们的代码会编译失败, 而不是悄悄产生输出。它们的默认目标是原生标准流,不是 C 的流, 所以即使 C 运行时不在可用状态,它们仍然能工作。


debug_println("state = ", state);

不要把 debug_print 当日志 API 用。它是开发工具,可用性刻意绑定构建模式。

7. 什么场景用什么

需求 使用
普通程序输出 print / println,目标为默认或显式文件/设备
崩溃后仍需保留的诊断 perr / perrln
不可能状态 panic / panicln
仅开发期的跟踪 debug_print / debug_perr
搬字节,不是搬值 transmit(见 第11.5章)
locale 感知的数字或时间 imbue(loc, out)(见 第26.1章)

要点