第 25 篇 给服务加了持久化;本篇是两个经典练手项目:LRU 缓存练数据结构设计,JSON 解析器练递归下降与 std::variant——都是面试高频、又能深入理解 C++ 现代特性。
demo:ref/cpp_demo/projects/learning_guide/(lru_cache/ + json_parser/)。
这是「现代 C++ 实战」系列的第 26 篇。建议先读 第 20 篇:栈、队列与链表(LRU 底层)和 第 24 篇:HTTP 服务与 JSON(nlohmann/json)。
一、LRU 缓存:设计思路
LRU(Least Recently Used)——缓存满时淘汰最久未使用的项。要求 get 和 put 均 O(1)。
1 2 3 4 5 6
| unordered_map<K, iterator> list<pair<K,V>> ┌─────────────────┐ ┌──────────────────────┐ │ key1 → iter1 ───┼─────────>│ (k1,v1) ↔ (k2,v2) ...│ │ key2 → iter2 ───┼─────────>│ MRU ──────────> LRU │ └─────────────────┘ └──────────────────────┘ front = 最近使用 back = 待淘汰
|
| 组件 |
作用 |
std::list |
维护访问顺序,支持 O(1) 移动 |
std::unordered_map |
key → list 迭代器,O(1) 查找 |
list::splice |
将节点移到头部,迭代器不失效 |
二、LRU 核心实现
src/lru_cache/lru_cache.hpp(节选,位于命名空间 cpp_learning):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| template<typename K, typename V> class LRUCache { public: using KeyValuePair = std::pair<K, V>; using ItemList = std::list<KeyValuePair>; using ListIterator = typename ItemList::iterator; using CacheMap = std::unordered_map<K, ListIterator>;
explicit LRUCache(size_t capacity) : capacity_(capacity) { if (capacity == 0) { throw std::invalid_argument("LRU cache capacity must be greater than 0"); } }
LRUCache(const LRUCache&) = delete; LRUCache& operator=(const LRUCache&) = delete; LRUCache(LRUCache&&) = default; LRUCache& operator=(LRUCache&&) = default;
[[nodiscard]] std::optional<V> get(const K& key) { auto it = cache_.find(key); if (it == cache_.end()) return std::nullopt; move_to_front(it->second); return it->second->second; }
void put(const K& key, V value) { if (auto it = cache_.find(key); it != cache_.end()) { it->second->second = std::move(value); move_to_front(it->second); return; } if (items_.size() >= capacity_) evict_lru(); items_.emplace_front(key, std::move(value)); cache_[key] = items_.begin(); }
private: size_t capacity_; ItemList items_; CacheMap cache_;
void move_to_front(ListIterator it) { items_.splice(items_.begin(), items_, it); }
void evict_lru() { if (items_.empty()) return; cache_.erase(items_.back().first); items_.pop_back(); } };
|
此外还有 get_ref(返回 std::optional<std::reference_wrapper<V>>,可原地修改)、contains(不改变访问顺序)、erase、clear、for_each、most_recent_key / least_recent_key 等辅助接口。
现代 C++ 特性一览:
| 特性 |
用途 |
std::optional<V> |
get 未命中返回 nullopt |
std::move |
put 避免值拷贝 |
[[nodiscard]] |
强制检查返回值 |
| 删除拷贝 / 默认移动 |
避免迭代器失效 |
demo 还实现了 LRUCacheWithCallback——构造时传入 std::function<void(const K&, V&)>,淘汰时触发回调,适合资源清理场景。
三、LRU 应用场景
| 场景 |
说明 |
| DNS 缓存 |
域名 → IP,减少查询延迟 |
| 页面缓存 |
Web 服务器热点数据 |
| 数据库缓冲池 |
磁盘页缓存 |
| CPU Cache |
硬件级 LRU 近似 |
demo 的 demo_dns_cache() 用 LRUCache<std::string, std::string>(容量 1000)模拟 DNS 缓存:第一次查询走慢路径,第二次缓存命中。
四、JSON 解析器:递归下降
第 24 篇 用 nlohmann/json 开箱即用;本篇手写解析器理解原理。
JSON 文法(简化):
1 2 3 4
| value := null | bool | number | string | array | object array := '[' (value (',' value)*)? ']' object := '{' (pair (',' pair)*)? '}' pair := string ':' value
|
每个规则对应一个解析函数——递归下降(src/json_parser/json_parser.hpp):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| [[nodiscard]] JsonValue parse_value() { skip_whitespace(); if (is_end()) { throw JsonParseError("Unexpected end of input", pos_); }
char c = peek(); switch (c) { case 'n': return parse_null(); case 't': return parse_true(); case 'f': return parse_false(); case '"': return parse_string(); case '[': return parse_array(); case '{': return parse_object(); case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return parse_number(); default: throw JsonParseError( std::string("Unexpected character: '") + c + "'", pos_); } }
|
parse_array 和 parse_object 递归调用 parse_value(),自然处理任意嵌套。
五、std::variant 表示 JSON 值
src/json_parser/json_value.hpp 中 JsonNull 即 std::monostate,JsonArray / JsonObject 分别是 std::vector<JsonValue> / std::map<std::string, JsonValue>,struct JsonValue : JsonVariant 继承 variant 并加上 is_xxx() / as_xxx() 等便捷方法:
1 2 3 4 5 6 7 8
| using JsonVariant = std::variant< JsonNull, bool, double, std::string, JsonArray, JsonObject >;
|
| JSON 类型 |
C++ 类型 |
variant index |
| null |
std::monostate |
0 |
| boolean |
bool |
1 |
| number |
double |
2 |
| string |
std::string |
3 |
| array |
vector<JsonValue> |
4 |
| object |
map<string, JsonValue> |
5 |
访问方式:
1 2 3 4 5 6 7 8 9
| std::visit([](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, bool>) { } else if constexpr (std::is_same_v<T, double>) { } }, static_cast<const JsonVariant&>(value));
if (val.is_string()) { auto s = val.as_string(); }
|
六、移动语义在解析中的应用
构建嵌套 JSON 时用 std::move 避免深拷贝:
1 2 3 4 5 6 7
| JsonArray arr; arr.push_back(parse_value());
return JsonValue(std::move(arr));
obj[std::move(key)] = std::move(value);
|
字符串解析末尾:
1
| return JsonValue(std::move(result));
|
七、错误处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class JsonParseError : public std::runtime_error { public: JsonParseError(const std::string& message, size_t position) : std::runtime_error(format_message(message, position)) , position_(position) {} [[nodiscard]] size_t position() const { return position_; } private: size_t position_; };
auto doc = JsonParser::parse(R"({"name": "Alice"})");
auto maybe = try_parse_json("invalid");
|
常见错误:尾随逗号、单引号、未闭合括号、undefined。
八、与 nlohmann/json 对比
|
手写解析器 |
nlohmann/json |
| 目的 |
学习、可控 |
生产使用 |
| 依赖 |
无(仅 STL) |
头文件库 |
| 性能 |
demo 级 |
高度优化 |
| API |
自定义 |
成熟生态 |
工程上直接用 nlohmann/json;理解递归下降和 variant 后,读任何 JSON 库源码都更容易。
九、运行 demo
1 2 3 4 5 6
| cd ref/cpp_demo/projects/learning_guide ./build.sh ./build/cpp_learning_guide --list ./build/cpp_learning_guide --demo lru ./build/cpp_learning_guide --demo json ./build.sh --run-args "--demo lru"
|
程序没有交互菜单,用 CLI11 解析 -d,--demo <name>,可选值 lru、json、coroutine、http、allocator、actor、database、regex。
LRU 演示:基本操作、std::optional 用法、淘汰策略、带回调的缓存、复杂类型、性能测试(容量 1 万、百万次 put/get)、DNS 缓存,最后跑一组自测。
JSON 演示:基本类型、数组、对象、std::visit、错误处理、序列化 dump()、与 nlohmann 对比,最后跑一组自测。
十、小结
| 项目 |
要点 |
| LRU |
unordered_map + list + splice,O(1) get/put |
| JSON 解析 |
递归下降,一文法一函数 |
| JsonValue |
std::variant 六种类型 |
| 移动语义 |
嵌套结构构建时避免拷贝 |
| 工程选型 |
学习手写,生产用 nlohmann/json |
下一篇进入系列收官:C++20 协程与 Actor 模型——见 第 27 篇:协程与 Actor 模型。