缓存类应使用双模板参数template,而非单参数;键值类型分离、支持自定义键哈希、lru用list+unordered_map组合、读写分离锁、构造函数用标签类型区分容量与初始化。

缓存类必须用模板,但不能只靠 template<typename t></typename>
单靠一个类型参数 T 无法满足“任意类型”需求——缓存的键(key)和值(value)往往不同,比如用 std::string 当 key、std::vector<int></int> 当 value。硬写成 template<typename t></typename> 会逼你把 key 和 value 绑死为同一类型,或者反复 cast,既不安全又难维护。
正确做法是显式分离两个模板参数:template<typename keyt typename valuet></typename>。这样 key 可以是 int、std::string 或自定义结构体(只要支持哈希和比较),value 可以是任意可拷贝/可移动类型。
- 如果 key 是自定义类型,必须提供
std::hash<keyt></keyt>特化或传入哈希函数对象 - value 类型无需特殊约束,但若涉及深拷贝大对象,建议检查是否支持移动语义(
ValueT有移动构造函数) - 别用
auto推导返回值类型——缓存 get 操作可能返回std::optional<valuet></valuet>或抛异常,类型必须明确
LRU淘汰策略要用 std::list + std::unordered_map 组合
只用 std::unordered_map 实现 O(1) 查找,但没法按访问顺序排序;只用 std::list 能维护顺序,但查找是 O(N)。两者结合才是标准解法:map 存 KeyT → std::list<:pair valuet>>::iterator</:pair>,list 存实际数据并保证最近访问在尾部。
注意几个关键细节:
- 每次
get()命中后,要把对应节点从 list 中摘出再splice()到尾部,而不是 erase+push_back——前者是常数时间,后者可能触发内存重分配 -
put()时若容量超限,删的是 list 头部(最久未用),同时从 map 中 erase 对应 key - map 的 value 类型必须是迭代器,且该迭代器生命周期要稳定——
std::list迭代器在插入/删除其他元素时不失效,这点比std::vector安全得多
线程安全不能靠 mutable std::mutex 粗暴包裹所有操作
加 mutex 能避免崩溃,但会严重拖慢吞吐量:哪怕只是并发读,也会被串行化。实际项目中读远多于写,应该优先支持无锁读。
更合理的分层设计:
- 基础版本先不做线程安全,文档注明“非线程安全”,方便单元测试和嵌入式场景
- 需要并发时,用
std::shared_mutex(C++17):shared_lock用于get(),unique_lock用于put()和evict() - 避免在
get()中锁住整个缓存——如果 value 类型本身是共享指针(如std::shared_ptr<bigobject></bigobject>),锁只需保护 map 和 list 结构,value 的访问可在外层放开 - 不要在析构函数里锁 mutex——若此时有其他线程正调用
put(),可能死锁
构造函数参数设计要区分“容量”和“初始元素”
用户初始化缓存时,常见两种需求:指定最大容量(如 1000 条),或预加载一批数据(如从配置文件读 key-value 对)。混在一起写成 Cache(int cap, std::initializer_list<...> init)</...> 会导致签名爆炸、调用歧义。
推荐用标签类型做参数重载:
struct capacity_t {};
struct init_list_t {};
Cache(capacity_t, size_t cap) : max_size_(cap) {}
Cache(init_list_t, std::initializer_list<:pair valuet>> init)
: max_size_(init.size()) {
for (auto& p : init) put(p.first, p.second);
}</:pair>
这样调用清晰:Cache<int std::string>(capacity_t{}, 512)</int> 或 Cache<:string double>(init_list_t{}, {{"a", 1.0}, {"b", 2.0}})</:string>。比用 std::optional<size_t></size_t> 或默认参数更不容易传错。
真正容易被忽略的是:缓存类不该假设 key 可默认构造。很多 key 类型(如 std::string_view 或包装了 non-copyable handle 的结构)没有默认构造函数,所以所有接口(get、put)的 key 参数必须是 const 引用或转发引用,不能出现 KeyT key = KeyT{} 这类兜底逻辑。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











