第20.13章:fast_io 是一个独立实现的 I/O 库
1. 论断
fast_io 是一个在下面没有操作系统时也能工作的 I/O 库。不是"格式化部分只有头文件,所以只要避开有意思的部分就能用" —— 而是整个库,包括容器、格式化、扫描和转码,把依赖操作系统的部分隔离在目标提供的类型参数后面。
本页是本章的回报,所以它写成四个设计决定,每一个都是对前面几页实测到的失败的正面回答:
| 失败 | fast_io 的回答 |
|---|---|
| 一个隐藏的全局堆(20.7) | 分配器是一个类型参数,并且被封进容器类型里 |
| 错误处理需要一个 OS(20.9) | 返回路径上的赫布异常,策略由分配器选定 |
| 消息表消耗 flash(20.10) | 一个只报告 [domain](code) 的构建开关,以及可覆盖的错误域 |
| 库头文件假定了一个控制台(20.6) | 设备也是一个类型 —— 一次 scatter 写入,或者干脆什么都没有 |
2. 分层在头文件里就看得见
fast_io 有三个总头文件,它们之间的分界就是独立与宿主的边界被显式写出来:fast_io_freestanding.h 用于向缓冲区格式化与扫描,fast_io_hosted.h 用于设备、文件和控制台,fast_io.h 两者兼有。
同样的划分也出现在容器上,而且它是被预处理器强制的,不是被文档强制的。::fast_io::containers::vector<T, Alloc> 永远可用;而那个便利别名 ::fast_io::vector<T>(它挑选平台的默认堆)只在有宿主平台时存在:
#if ((__STDC_HOSTED__ == 1 && (!defined(_GLIBCXX_HOSTED) || _GLIBCXX_HOSTED == 1) && \
!defined(_LIBCPP_FREESTANDING)) || \
defined(FAST_IO_ENABLE_HOSTED_FEATURES))
namespace fast_io
{
template <typename T, typename Alloc = ::fast_io::native_global_allocator>
using vector = ::fast_io::containers::vector<T, Alloc>;
所以在独立目标上,::fast_io::vector 不存在,而编译器用最清楚的方式说出来 —— 一个找不到的名字,旁边建议着 ::fast_io::containers::vector。不会有悄悄回退到隐藏堆的事,因为根本没有名字可以回退。你写 ::fast_io::containers::vector<int, my_arena_allocator>,而分配器是类型的一部分。
3. 堆来自目标
默认分配器正是按你希望的那个条件选出来的。当平台是宿主时它是 c_malloc_allocator(或者 NT 堆,或者 mimalloc,或者任何被配置的东西);当它是独立时它是 freestanding_global_allocator:
using native_global_allocator = generic_allocator_adapter<
#if defined(FAST_IO_USE_CUSTOM_GLOBAL_ALLOCATOR)
custom_global_allocator
#elif ...
c_malloc_allocator
#else
freestanding_global_allocator
#endif
>;
而 freestanding_global_allocator 在库里只有声明。它的三个函数在 fast_io 里没有任何定义:
class freestanding_global_allocator
{
public:
static ::fast_io::allocation_least_result allocate_aligned_conditional_zero_at_least_die(::std::size_t alignment, ::std::size_t n, bool zero) noexcept;
static ::fast_io::allocation_least_result allocate_aligned_conditional_zero_at_least_try(::std::size_t alignment, ::std::size_t n, bool zero) FAST_IO_HERBCEPTIONS_THROWS;
static void deallocate_aligned_n(void* p, ::std::size_t alignment, ::std::size_t n) noexcept;
};
这就是独立分配器的全部契约:带对齐的分配、可选择清零、返回 {指针, 数量},或者抛一个赫布异常,或者死掉。你为你的目标实现它 —— 一个 bump arena、一个 slab、一个 per-CPU 池,目标有什么就用什么 —— 而每一个 fast_io 容器、每一个需要临时内存的格式化例程,用的都正好是它,没有别的。一个没有你提供堆就无法链接的库,就是一个无法在你背后分配的库。
4. 生成的代码要求什么:什么都不要求
下面是一个向栈缓冲区格式化并使用带调用者提供的 arena 的容器的程序。它只有一个翻译单元,只包含 fast_io_core.h 和 vector,而且从不接触文件、控制台或 locale:
#include <fast_io_core.h>
#include <fast_io_dsal/vector.h>
template <::std::size_t extent>
class arena {
public:
alignas(::std::max_align_t) static inline ::std::byte storage[extent]{};
static inline ::std::size_t used{};
static ::fast_io::allocation_least_result
allocate_aligned_conditional_zero_at_least_die(::std::size_t alignment,
::std::size_t n, bool) noexcept {
::std::size_t const start{(used + alignment - 1u) / alignment * alignment};
if (start + n > extent) { ::fast_io::fast_terminate(); }
used = start + n;
return {storage + start, n};
}
static void deallocate(void *) noexcept {}
static void deallocate_aligned_n(void *, ::std::size_t, ::std::size_t) noexcept {}
};
using heap_a = ::fast_io::generic_allocator_adapter<arena<4096>>;
int render(int value) throws {
char buffer[64];
::fast_io::containers::vector<int, heap_a> v;
v.push_back(value);
::fast_io::basic_obuffer_view<char> out{buffer, buffer + sizeof(buffer)};
::fast_io::operations::print_freestanding<false>(out, "v[0] = ", v[0], "\n");
return static_cast<int>(out.size());
}
int main() {
try { return render(12345); }
catch throws (::std::error) { return -1; }
}
$ g++ -std=c++26 -fherbceptions -O2 -I fast_io/include -c core2.cc -o core2.o
$ nm -u core2.o
[0 undefined symbols]
$ size core2.o
1444 0 4104 5548 15ac core2.o
零。没有 malloc、没有 memcpy、没有 __cxa_*、没有 unwinder、没有 write、没有 locale、没有 __gxx_personality_v0。1444 字节的代码和 4 KiB 的 arena 就是整个程序:整数格式化、一个可增长的容器,以及一条错误通道。那个目标文件向世界要求的唯一东西是一个栈。
而当 arena 用尽时,失败策略就写在 arena 里:上面那个 _die 口味调用 fast_terminate()(19.2);_try 口味则在 throws 通道上报告 ::std::errc::not_enough_memory(19.9)。选哪一个是对每个容器类型在编译期做的决定,不是全局开关,而这就是为什么一个 fast_io 容器可以安全地交给一个承诺过不分配内存的子系统。
5. 为什么这就是该用的库
宿主程序在不知情的情况下从这一切中受益:没有隐藏分配、没有 locale、没有 iostream 初始化、失败路径上没有异常,以及一个直接把内容写进目标而不是为每个实参构造一个临时 std::string 的格式化引擎(17.8 展示了 + 那个版本要付多少代价)。然后,当代码哪天要跑在板子上时,什么也不用改:同样的头文件、同样的 print、同样的容器,分配器和设备由目标提供。
这就是本章一直在做的论证的实践形态。标准库的 I/O 和容器是为宿主程序设计的,并且永远不会成为独立的(20.6),因为它们的接口就是隐藏的堆和抛出的异常。fast_io 的接口是分配器和设备,所以它在边界的两侧都能工作 —— 这就是为什么本书用它做所有事,而不是把它当成一个更快的 <iostream>。