std::forward_list::size() 不可信,因libstdc++中为O(n)遍历实现;C++17才要求O(1),旧标准下应始终用empty()判空,避免性能陷阱与未定义行为。

std::forward_list 不是 std::list 的轻量替代品,它只在“大量小节点 + 高频头插/头删 + 无长度需求”三者同时成立时才真正省内存、真高效;其他情况用它反而增加维护成本和运行时风险。
为什么 std::forward_list::size() 不能信
libstdc++(GCC 默认 STL)至今不维护计数器,fl.size() 就是 std::distance(fl.begin(), fl.end()),纯 O(n) 遍历。C++17 要求可实现为 O(1),但很多项目仍在用 C++14 或嵌入式裁剪版 STL,此时调用 fl.size() 等于隐式触发一次全链表扫描。
- 判空必须写
fl.empty()—— 它始终 O(1) 且 guaranteed defined - 别写
if (fl.size() == 0),哪怕编译通过,也可能返回垃圾值或抛异常 - 循环里反复调用
fl.size()(比如for (size_t i = 0; i )会让 O(n) 操作退化成 O(n²) - 真需要长度且高频?手动维护一个
size_t count,每次emplace_front()、insert_after()、erase_after()后同步增减
insert_after() 和 erase_after() 的参数永远指“前驱”,不是目标节点
这是单向链表结构限制导致的硬约束,不是 API 设计疏漏。你无法从任意节点反推前驱,所以所有中间操作都必须显式传入前驱迭代器。
- 头插唯一安全写法:
fl.insert_after(fl.before_begin(), val)或更简洁的fl.emplace_front(val) - 删首节点必须用
fl.erase_after(fl.before_begin());传fl.begin()是错的——它指向第一个有效节点,不是前驱 - 删第 n 个元素:先
auto prev = std::next(fl.before_begin(), n - 1),再fl.erase_after(prev)(注意是 n−1,不是 n) - 传
fl.end()给erase_after()是未定义行为;传fl.begin()会删第二个元素,不是第一个
splice_after() 是唯一不可替代的 O(1) 拼接操作
splice_after() 不调用构造/析构、不分配内存、不拷贝数据,只改 next 指针。它是 std::forward_list 唯一真正物理拼接的能力,其他容器无法低成本模拟。
立即学习“C++免费学习笔记(深入)”;
-
dst.splice_after(pos, src):把整个src接到dst中pos后,src变为空 -
dst.splice_after(pos, src, first, last):只拼接src中[first, last)范围(左闭右开) - LRU 缓存中把命中节点移到头部:需配合手动维护头尾逻辑,用
fl.splice_after(fl.before_begin(), fl, it) - 自拼接(
fl.splice_after(fl.before_begin(), fl))是未定义行为;src和dst必须是不同对象
什么时候该放弃 std::forward_list
它不是通用链表,而是一个有明确取舍的专用工具。一旦出现以下任一情况,就该换容器:
- 需要
push_back()或访问back()——std::forward_list没有尾指针,尾部操作是 O(n) - 需要按索引访问(
operator[])、反向遍历(rbegin())、或频繁查长度 —— 它都不支持 - 存的是
std::string或大结构体(如 1KB)—— 8 字节指针差异可忽略,别硬换 - 实际节点大小没省多少?用
sizeof实测你的std::forward_list<T>::node,别依赖文档或经验;allocator 对齐可能影响结果
最常被忽略的一点:它不提供任何缓存友好性,随机访问性能比 std::vector 差得多;如果你的场景本质是“顺序读+偶尔中间删”,但又需要长度或尾操作,std::list 或带手动 size 计数的 std::vector 往往更稳。


















