第9.16章: C++ 标准库容器

English

概述

C++ 标准库在 std:: 命名空间中自带一套容器。如果你来自其他语言,或者以前没用过 C++,可能会在现有代码库里遇到它们。本节介绍它们,并说明它们与 fast_io 有何不同。

一句话:优先使用 fast_io 容器。它们更安全、更快、更一致。但你仍应能认出 std:: 中的对应物,以便读懂别人的代码、理解整个生态。

1. 可用的容器

std:: 容器 fast_io:: 中的对应物 说明
std::vector<T> ::fast_io::vector<T> 动态连续数组。
std::deque<T> ::fast_io::deque<T> 双端队列(块映射布局)。
std::list<T> ::fast_io::list<T> 双向链表。
std::forward_list<T> ::fast_io::forward_list<T> 单向链表。
std::array<T, N> ::fast_io::array<T, N> 固定大小数组。
std::string ::fast_io::string 动态的、以空字符结尾的字符序列。
std::string_view ::fast_io::string_view 对字符的非拥有视图。
std::span<T> ::fast_io::span<T> 对连续序列的非拥有视图。
std::stack<T> ::fast_io::stack<T> LIFO 适配器。
std::queue<T> ::fast_io::queue<T> FIFO 适配器。
std::priority_queue<T> ::fast_io::priority_queue<T> 基于优先级的适配器。

2. 基本用法

std:: 容器的用法与 fast_io 的容器大体相同。主要差别在于头文件路径,以及缺少一些 fast_io 特有的功能:


#include <vector>
#include <deque>
#include <string>
#include <iostream>

int main() {
    // std::vector — dynamic contiguous array
    ::std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    ::std::cout << "Size: " << v.size() << '\n';

    // std::deque — double-ended queue
    ::std::deque<int> d;
    d.push_front(10);
    d.push_back(20);

    // std::string — dynamic character sequence
    ::std::string s = "Hello";
    s += ", World!";
    ::std::cout << s << '\n';
}
注意:本教程通篇使用 fast_io。你不需要为了跟上进度而去学习 std:: 容器 — 我们讲的所有内容都用 fast_io。

3. 关键差异

如果你确实遇到 std:: 容器,请注意这些差异:

方面 std:: 容器 fast_io:: 容器
边界检查 operator[] 不做检查 — 越界是未定义行为。at() 会抛出 std::out_of_range,但边界检查本身带来可观的开销。更糟的是,越界访问本身就已经是 bug — 在那一刻捕获异常并不能修正底层问题。试图从程序 bug 中恢复,比直接终止并重启更糟。 operator[] 带边界检查。越界会立刻调用 fast_terminate() — 行为确定、没有异常开销、也不会给人错误的“已恢复”幻觉。
分配失败 抛出 std::bad_alloc。堆分配失败意味着抽象机已经损坏 — 没有有意义的方式可以继续。抛异常只会招来那些试图在已经损坏的堆上勉强前行的错误恢复代码。 调用 fast_terminate() — 不展开异常,也不试图从不可恢复的状态中恢复。程序已经损坏了,就让它确定性地崩溃。
逻辑 bug(空的 front/back、越界) 未定义行为。在发行版构建中会悄悄破坏内存。 确定性地终止。bug 立刻被发现。
增长时的搬移 使用移动构造函数。对移动非平凡的类型来说,这可能很慢。 当类型可平凡搬移时使用 memcpy — 快得多。
基于下标的操作 没有 insert_index / erase_index,必须使用迭代器。 在所有适用的容器上都提供带边界检查的 insert_index / erase_index。
字符串格式化 std::format(C++20)或 std::stringstream — 相对较慢。 concat_fast_io + to<T> — 在编译期优化。
非检查变体 没有。只有 operator[] 是非检查的,其余都通过异常做检查。 为性能关键的代码提供显式的 _unchecked 变体。
位向量 std::vector<bool> 是一个打包了位、但接口很糟糕的特化(代理引用)。 ::fast_io::bitvec 是一个独立、干净的位级类型,具有正确的按位操作。

4. 关于 C++26 的加固

C++26 为标准库容器引入了加固 — 对 operator[]、front()、back() 做边界检查,并增加迭代器有效性断言。然而这些容器在设计之初并不是为了被加固的。把安全检查补丁打到几十年的既有 ABI 上会带来问题:

fast_io 容器从一开始就把安全设计进去 — 没有 ODR 问题,没有“可选”带来的混乱,并且在所有构建下都一致地确定性终止。

5. 代码对比

下面是同一个程序分别用两个库写的版本:

使用 fast_io(推荐)


#include <fast_io_dsal/vector.h>
#include <fast_io_dsal/string.h>
#include <fast_io.h>

int main() {
    ::fast_io::vector<::std::size_t> v;
    v.push_back(1zu);
    v.push_back(2zu);
    v.push_back(3zu);

    // Bounds-checked — terminates on out-of-bounds
    ::fast_io::io::println("v[0] = ", v[0zu]);

    // Index-based insert and erase
    v.insert_index(1zu, 99zu);  // [1, 99, 2, 3]
    v.erase_index(0zu);         // [99, 2, 3]

    // String formatting
    ::fast_io::string s = ::fast_io::concat_fast_io("Count: ", v.size());
    ::fast_io::io::println(s);
}

使用 std::


#include <vector>
#include <string>
#include <format>
#include <iostream>

int main() {
    ::std::vector<::std::size_t> v;
    v.push_back(1zu);
    v.push_back(2zu);
    v.push_back(3zu);

    // operator[] is unchecked — UB if out of bounds
    ::std::cout << "v[0] = " << v[0] << '\n';
    // v.at(0) is checked but throws an exception

    // Must use iterators for insert/erase
    v.insert(v.begin() + 1, 99zu);   // [1, 99, 2, 3]
    v.erase(v.begin());               // [99, 2, 3]

    // String formatting
    ::std::string s = ::std::format("Count: {}", v.size());
    ::std::cout << s << '\n';
}

6. 延伸阅读

std:: 容器的完整文档见 cppreference — Container library。它对每个 std:: 类型、方法和重载都给出详尽参考。

不过,在本教程项目的新代码里,请总是优先使用 fast_io 的对应物。

要点