《C++ 项目:手写大模型接入 SDK》SessionManager问题发现、产生原因及补救措施
在上篇文章SessionManager会话管理模块:架构设计与实现的结尾,我们展示了接口实现的单元测试结果,如下图所示:

我们会发现测试是通过的,并没有代码运行的问题,但是我们看到图中在获取所有会话ID列表的打印结果中,虽然只有两个会话ID,但是却凭空多出了一些空行,在上篇文章当时我并没有觉得有什么太大的问题,觉得就是打印的时候一个小 bug 没在意。
但是后来我拿着会话ID列表数组去访问每个会话的历史消息时程序就崩溃了,而产生上面图中的结果已经程序崩溃的原因就是因为 getSessionLists 接口设计的有问题。
问题现象复盘
从日志表面看,程序可以正常跑完 GTest,最终测试标记 PASS,没有直接崩溃。 但容器内部已经出现脏数据:
我们预期temp容器里面只有 2 条会话数据,而实际容器内一共存在 8 个元素,也就是说那些空行就是一个一个vector中的元素。
在打印输出的时候,遍历整个temp,空 pair 的第一个字段是空字符串,打印出来就表现为肉眼看到的空行。
最危险的地方:这个错误不会触发编译报错,短时间也不一定触发崩溃,属于典型的静默逻辑 bug。
当后续业务逻辑去读取、访问这些空会话元素、尝试去拿到Session对象的成员时,就会访问非法内存,直接触发程序崩溃。
getSessionLists 原始错误写法
// 获取所有会话ID列表:返回所有当前存在的会话ID
std::vector<std::string> SessionManager::getSessionLists() const
{
// 加锁保护,确保在多线程环境下对获取会话列表是线程安全的,并且在函数退出时自动释放锁
std::lock_guard<std::mutex> lock(_mutex);
// 根据更新时间戳对会话ID进行降序排序,保证最新的会话ID在列表的最前面
std::vector<std::pair<std::string, std::time_t>> temp(_sessions.size());
for (auto pair : _sessions)
{
temp.emplace_back(pair.first, pair.second->_updated_at);
}
std::sort(temp.begin(), temp.end(), [](const auto &a, const auto &b)
{ return a.second > b.second; });
// 从排序后的会话ID-时间戳对中提取排完序的会话ID,构建会话ID列表
std::vector<std::string> sessionIds(temp.size());
for (const auto &pair : temp)
{
sessionIds.push_back(pair.first);
}
return sessionIds;
}
问题产生原因
vector<T>(count)这个带参构造,会直接创建 count 个默认元素temp.size()初始就等于_sessions.size(),里面每一个 pair 都是默认构造:空字符串、time_t=0- 容量 capacity 和 size 相等
- 循环里继续调用
emplace_back:是追加到容器尾部,不是覆盖原有元素- 最终 temp 的元素数量 =
_sessions.size() + _sessions.size(),翻倍 - 前半部分全部是默认空值,后半部分才是我们真正塞进去的会话 + 时间,排序的时候空数据也参与排序,逻辑完全错乱,这就是致命问题。
- 最终 temp 的元素数量 =
- 这种初始化方式适合:用下标
temp[i] = xxx直接覆盖赋值的场景,不适合搭配emplace_back/push_back。
一句话总结:只要打算循环用 emplace_back 往里面填数据,就不能用
vector(N)这种方式初始化。
这也是为什么上面我们的结果展示中会显示那么多空行的直接原因。
正确写法:reserve 预分配方案
// 获取所有会话ID列表:返回所有当前存在的会话ID
std::vector<std::string> SessionManager::getSessionLists() const
{
// 加锁保护,确保在多线程环境下对获取会话列表是线程安全的,并且在函数退出时自动释放锁
std::lock_guard<std::mutex> lock(_mutex);
// 根据更新时间戳对会话ID进行降序排序,保证最新的会话ID在列表的最前面
std::vector<std::pair<std::string, std::time_t>> temp;
temp.reserve(_sessions.size()); // 预留足够的空间,避免多次内存分配
// 注意:不能直接使用带参数的构造函数,因为会立即构造对象,导致temp已经包含了_sessions实际的元素个数
// 如果再使用插入接口,会导致temp的大小再次增加,与_sessions的大小不一致!并且前面的元素都是默认的空值元素
for (auto pair : _sessions)
{
temp.emplace_back(pair.first, pair.second->_updated_at);
}
std::sort(temp.begin(), temp.end(), [](const auto &a, const auto &b)
{ return a.second > b.second; });
// 从排序后的会话ID-时间戳对中提取排完序的会话ID,构建会话ID列表
std::vector<std::string> sessionIds;
sessionIds.reserve(temp.size());
for (const auto &pair : temp)
{
sessionIds.push_back(pair.first);
}
return sessionIds;
}
原理说明
reserve(n):仅仅申请底层连续内存,不会调用元素的构造函数- 执行完
reserve之后:temp.size() = 0,temp.capacity() = _sessions.size() - 容器里没有任何有效元素,内存只是提前准备好了,避免循环 emplace_back 时多次扩容、内存拷贝
- 执行完
emplace_back会从容器起始位置依次放入元素,最终 size 刚好等于_sessions.size(),没有多余空元素- 性能优势:一次性分配内存,规避 vector 动态扩容带来的多次内存重分配,在会话数量很多的时候收益明显。
两种 vector 初始化方式对比
| 方式 | size 初始值 | capacity | 是否构造元素 | 适配接口 |
|---|---|---|---|---|
vector<T>(N) | N | N | 是,全部默认构造 | temp[i] = xxx 下标覆盖赋值 |
reserve(N) | 0 | N | 否 | push_back / emplace_back 尾部追加 |
正确运行结果展示:

我们会发现空行也就没有了,那么后续这个vector数据内部也就不存在无效的元素,通过会话ID访问会话消息也就不会产生程序崩溃的情况了。
小结
这个坑是 C++ STL 容器开发中非常容易忽略的点:很多人误以为vector(n)只是单纯预留内存,实际上它会直接构造元素。
记住一条规范:如果你的填充方式是循环 push_back /emplace_back,就使用 reserve 预分配,不要使用 vector (n) 初始化。
这类隐性 bug,不会立刻崩溃,只会在后续访问脏元素的时候随机触发宕机,排查成本很高,在 SDK 开发中需要重点规避。
更多推荐


所有评论(0)