std::list + std::unordered_map 是主流选择,因 list 支持 o(1) splice/erase 且迭代器稳定,unordered_map 提供 o(1) key→iterator 查找;其他容器易导致迭代器失效或复杂度上升。

为什么 std::list + std::unordered_map 是主流选择
因为 LRU 的核心操作是「频繁移动节点到头部」和「O(1) 查找+删除尾部」,std::list 提供稳定的迭代器和 O(1) 的 splice/erase,std::unordered_map 提供 O(1) 的 key→node 指针映射。用 std::vector 或 std::deque 会导致移动元素时迭代器失效或复杂度升至 O(N);手写链表容易在 erase 后悬空指针。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用
std::list<:pair value>></:pair>存数据,避免拷贝——把Value改成std::unique_ptr<value></value>或std::shared_ptr<value></value>,尤其当Value大于 16 字节时 -
std::unordered_map<key std::list>::iterator></key>的 value 类型必须是迭代器(不是指针),否则splice无法跨容器移动 - 别在
get()里先erase再push_front:这会触发两次内存分配;直接用splice(list.begin(), list, it->second)
如何避免迭代器失效导致的段错误
最常见坑是:在 put() 中插入新项后,旧的 map[key] 迭代器仍指向已被 erase 的节点,后续再解引用就崩。根本原因是 std::list::erase() 会使被删节点的迭代器立即失效,且不自动从 map 中清理。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 每次
erase前,先用map.erase(key)清掉映射——顺序不能反,否则 erase 后 map 里还存着野迭代器 - 如果支持重复
put()(即更新值),别直接map[key] = list.begin():这会触发默认构造+赋值,可能抛异常;改用map.insert_or_assign(key, list.begin())(C++17) - 调试时加断言:
assert(it != list.end() && map.count(key));在 get/put 开头检查
size_t 容量限制与内存碎片风险
LRU 缓存若不限制总字节数(只限 item 数量),实际内存可能远超预期——比如缓存 1000 个 std::string,每个平均 2KB,就占了 2MB;而一个 std::string 小于 23 字节时走 SSO,不分配堆内存,但超过后每次 new 都有 malloc 开销和碎片。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 不要只用
size()判断容量,加一个运行时累加的total_bytes_成员,在put()插入前检查是否超限 - 对
Value类型提供size_bytes()接口(可为模板特化),而不是粗暴调用sizeof(Value)——后者对含指针的类型完全没意义 - 避免在缓存中存裸
char*或std::vector<uint8_t></uint8_t>:它们的 size 不等于实际内存占用;优先用std::string_view+ 外部生命周期管理,或封装带 capacity 记录的 buffer 类
多线程下 get/put 的最小锁粒度
用全局 std::mutex 最简单,但会成为性能瓶颈——尤其读多写少场景下,所有 get() 都要抢同一把锁。而细粒度锁(如分段锁)又难保 LRU 顺序语义:两个线程同时 get() 同一个 key,可能一个刚 move 到头部,另一个又把它移回去。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 读操作(
get())本身无副作用,但「移动到头部」有——所以不能无锁读;但可以用std::shared_mutex(C++17),读用lock_shared(),写用lock() - 别给
std::list和std::unordered_map分开加锁:它们的操作必须原子,否则 map 迭代器和 list 节点状态会不一致 - 如果业务允许弱一致性(比如容忍短暂 miss),可考虑 RCU 风格:读路径无锁,写路径 copy-on-write 整个结构——但实现成本高,一般项目没必要
真正难的是容量淘汰时的原子性:evict 一个节点要同时从 list 删、从 map 删、释放 value 内存,这三步必须在一个锁区内完成,漏掉任何一步都会泄漏或崩溃。很多开源实现在这里栽过跟头。











