使用C++实现的错误信息收集器
作者:李迟
1. 背景
在大型C++项目的开发过程中,错误处理是一个不容忽视的核心环节。年初笔者接手了一个工程,近几个月在修改,发现其中的错误信息处理比较凌乱,考虑到方便日后的自己的维护,因此要进行必要的重构。缺乏统一错误收集机制带来以下一些困扰:
- 信息碎片化:不同模块各自定义错误格式,日志散落在不同文件中,排查问题时需要手动关联
- 重复记录:同一个错误可能被多个层级重复记录,导致日志膨胀
- 特殊字符干扰:错误消息中包含的换行符、制表符破坏日志格式,影响后续分析
- 缺乏分类:无法区分致命错误、警告和普通提示信息
为了解决这些问题,笔者着手设计并实现了一个轻量级的ErrorCollector类。本文将从需求分析、设计思路、实现细节到测试验证,完整分享这个组件的开发过程。
PS:近年来,笔者基本都会因其他共事的人升职、转岗等原因接手不同性质代码工程,但还没能力向他人移交工程或事务。从技术角度看,需要写不同类别的代码,是件好事,但从个人成长角度看,不是个事,特别是处于年龄越来越大,头发越来越白的尴尬的人生阶段。
2. 设计思路
2.1 核心需求
经过需求梳理,我们确定了以下核心功能点:
- 去重存储:相同的错误消息只保留一份,避免冗余
- 分类标记:每条消息关联一个错误码(0表示普通信息,非0表示错误)
- 字符清洗:自动移除
\t、\n、\r等特殊字符 - 灵活查询:支持按类型(全部/仅错误)、按错误码过滤查询
- 空值安全:对空字符串和空指针有防护
- 简单易用:提供直观的接口,且只有头文件,减少使用成本
2.2 数据结构设计
采用两个并行的std::vector分别存储错误码和消息文本:
std::vector<int> m_code; // 错误码列表 std::vector<std::string> m_list; // 消息列表(与m_code一一对应)
这种设计的优势有以下几点:
- 索引对应:通过相同下标关联错误码和消息,无需额外的映射结构(注意索引需大于等于0)
- 顺序保留:保持添加顺序,便于追溯问题发生的时间线
- 内存友好:相比
std::map或std::unordered_map,内存开销更小
去重机制则通过std::find在m_list中线性查找实现。虽然时间复杂度为O(n),但考虑到错误收集场景下消息数量通常有限(一般不超过几十条),这种实现足够高效且代码简洁。
2.3 接口设计原则
- 重载友好:同时支持
std::string和const char*参数,适应不同使用场景 - 默认参数:
code参数默认为0,简化普通信息记录的调用 - 防御性编程:在函数入口进行参数校验,提前返回避免无效操作
- const正确性:查询接口标记为
const,确保不修改对象状态
3. 实践
3.1 源码
ErrorCollector.h文件代码如下:
#include <algorithm>
#include <vector>
#include <unordered_set>
#include <string>
// 错误信息、过程信息记录器
class ErrorCollector {
public:
// 构造函数,初始化变量为空
ErrorCollector() {
m_code.clear();
m_list.clear();
}
// 析构函数(可选)
~ErrorCollector() {
m_code.clear();
m_list.clear();
}
void AddInfo(const std::string& msg1, int code = 0) {
// 判断字符串是否为空
if (msg1.empty()) {
return;
}
std::string msg = msg1;
msg.erase(std::remove_if(msg.begin(), msg.end(),
[](char c) { return c == '\t' || c == '\n' || c == '\r'; }),
msg.end());
// 删除特殊字符后再次判断是否为空
if (msg.empty()) {
return;
}
if (std::find(m_list.begin(), m_list.end(), msg) == m_list.end()) {
m_code.push_back(code);
m_list.push_back(msg);
}
}
void AddInfo(const char* msg1, int code = 0) {
// 判断指针是否为空,以及字符串是否为空
if (msg1 == nullptr || msg1[0] == '\0') {
return;
}
std::string msg = msg1;
AddInfo(msg, code);
}
// 返回竖线分隔的字符串,格式:错误1 | 错误2 | 错误3
// all 表示所有的信息,如只返回错误,设置为false
std::string GetInfos(bool all = true) const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_list.size(); i++) {
if (!all && m_code[i] == 0) {
continue;
}
if (cnt > 0) {
result += " | ";
}
cnt++;
result += m_list[i];
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
// 同上,指定code
std::string GetInfoByCode(int code) const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_list.size(); i++) {
if (m_code[i] != code) {
continue;
}
if (cnt > 0) {
result += " | ";
}
cnt++;
result += m_list[i];
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
std::string GetCodes() const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_code.size(); i++) {
if (m_code[i] == 0) {
continue;
}
if (cnt > 0) {
result += "|"; // 不要空格紧凑一些
}
cnt++;
result += std::to_string(m_code[i]);
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
// 可指定忽略的code
std::string GetCodes(int skipCode) const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_code.size(); i++) {
if (m_code[i] == 0 || m_code[i] == skipCode) {
continue;
}
if (cnt > 0) {
result += "|"; // 不要空格紧凑一些
}
cnt++;
result += std::to_string(m_code[i]);
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
bool Empty() {return m_list.empty();}
private:
std::vector<int> m_code;
std::vector<std::string> m_list;
};
3.2 关键实现解析
特殊字符清洗:
msg.erase(std::remove_if(msg.begin(), msg.end(),
[](char c) { return c == '\t' || c == '\n' || c == '\r'; }),
msg.end());
这里使用了经典的"erase-remove"惯用法,高效地移除了三种常见的格式干扰字符。清洗后再判断消息是否为空,确保不会存储仅由特殊字符组成的无效消息。
去重逻辑:
if (std::find(m_list.begin(), m_list.end(), msg) == m_list.end()) {
m_code.push_back(code);
m_list.push_back(msg);
}
值得注意的是,此处考虑的设计决策是:去重基于消息内容,而不是消息+错误码的组合。这意味着如果同一个错误消息先后以不同的错误码添加,只有第一次会被保留,后续的添加会被忽略。这种设计在某些场景下可能丢失错误码信息,但换来的是消息列表的简洁性。在实际使用中,我们约定错误消息应该包含足够的上下文信息,使得重复消息确实不需要再次记录。
输出格式控制:
if (cnt > 0) {
result += " | ";
}
cnt++;
result += m_list[i];
此处通过计数器控制分隔符的插入位置,避免在字符串开头或结尾出现多余的分隔符。GetCodes方法中则使用更紧凑的"|"(无空格)作为分隔符,方便后续解析。
3.3 使用示例
ErrorCollector collector;
// 记录普通信息
collector.AddInfo("开始数据迁移");
// 记录错误
collector.AddInfo("连接数据库失败", 1001);
collector.AddInfo("读取配置文件异常", 1002);
// 获取所有信息(包含提示和错误)
std::string all = collector.GetInfos(true);
// 输出: "开始数据迁移 | 连接数据库失败 | 读取配置文件异常"
// 仅获取错误信息
std::string errors = collector.GetInfos(false);
// 输出: "连接数据库失败 | 读取配置文件异常"
// 获取错误码列表
std::string codes = collector.GetCodes();
// 输出: "1001|1002"
// 按错误码查询
std::string info = collector.GetInfoByCode(1001);
// 输出: "连接数据库失败"
3.4 应用场景
ErrorCollector在以下场景中特别有用:
- API参数校验:收集所有校验失败的信息,一次性返回给调用方
- 批量操作:执行多个独立操作时,收集所有异常而非遇到第一个错误就中断
- 配置加载:加载配置文件时,记录所有格式错误或缺失的配置项
- 单元测试:收集测试过程中的断言失败信息
4. 测试
4.1 测试代码
测试代码文件main.cpp:
#include <iostream>
#include "ErrorCollector.h"
void printTestResult(const std::string& testName, bool passed) {
std::cout << (passed ? "[PASS]" : "[FAIL]") << " " << testName << std::endl;
}
int main() {
ErrorCollector collector;
// 测试1:基本添加和获取
std::cout << "\n=== 测试1:基本功能 ===" << std::endl;
collector.AddInfo("数据库连接失败", 1001);
collector.AddInfo("配置文件缺失", 1002);
collector.AddInfo("用户认证超时", 0); // code为0表示提示信息
std::string infos = collector.GetInfos(true);
std::cout << "所有信息: " << infos << std::endl;
std::cout << "仅错误: " << collector.GetInfos(false) << std::endl;
std::cout << "错误码: " << collector.GetCodes() << std::endl;
printTestResult("基本功能", infos == "数据库连接失败 | 配置文件缺失 | 用户认证超时");
// 测试2:去重功能
std::cout << "\n=== 测试2:去重功能 ===" << std::endl;
ErrorCollector collector2;
collector2.AddInfo("重复错误", 100);
collector2.AddInfo("重复错误", 200); // 相同消息,不同code
collector2.AddInfo("唯一错误", 300);
std::string result = collector2.GetInfos(true);
std::cout << "去重后结果: " << result << std::endl;
std::cout << "错误码: " << collector2.GetCodes() << std::endl;
printTestResult("去重功能", result == "重复错误 | 唯一错误" && collector2.GetCodes() == "100|300");
// 测试3:空字符串和特殊字符
std::cout << "\n=== 测试3:空值和特殊字符过滤 ===" << std::endl;
ErrorCollector collector3;
collector3.AddInfo("", 100);
collector3.AddInfo("\t\n\r", 200);
collector3.AddInfo("有效消息", 300);
collector3.AddInfo(nullptr, 400);
std::string result3 = collector3.GetInfos(true);
std::cout << "过滤后结果: " << result3 << std::endl;
printTestResult("空值过滤", result3 == "有效消息");
printTestResult("空状态检测", !collector3.Empty());
// 测试4:特殊字符清理(制表符、换行符)
std::cout << "\n=== 测试4:特殊字符清理 ===" << std::endl;
ErrorCollector collector4;
collector4.AddInfo("消息\t包含\t制表符", 100);
collector4.AddInfo("第一行\n第二行\r\n", 200);
std::string result4 = collector4.GetInfos(true);
std::cout << "清理后结果: " << result4 << std::endl;
printTestResult("制表符清理", result4 == "消息包含制表符 | 第一行第二行");
// 测试5:按错误码过滤
std::cout << "\n=== 测试5:按错误码获取 ===" << std::endl;
ErrorCollector collector5;
collector5.AddInfo("错误A", 100);
collector5.AddInfo("提示B", 0);
collector5.AddInfo("错误C", 200);
collector5.AddInfo("错误D", 300);
std::cout << "错误码100的信息: " << collector5.GetInfoByCode(100) << std::endl;
std::cout << "跳过错误码200: " << collector5.GetCodes(200) << std::endl;
printTestResult("按码获取", collector5.GetInfoByCode(100) == "错误A");
printTestResult("跳过码获取", collector5.GetCodes(200) == "100|300");
// 测试6:格式验证
std::cout << "\n=== 测试6:输出格式验证 ===" << std::endl;
ErrorCollector collector6;
collector6.AddInfo("A", 1);
collector6.AddInfo("B", 2);
collector6.AddInfo("C", 3);
std::string result6 = collector6.GetInfos(true);
std::cout << "格式结果: " << result6 << std::endl;
bool formatOk = (result6 == "A | B | C") &&
(result6.back() != '|') &&
(result6.front() != '|');
printTestResult("格式正确", formatOk);
// 测试7:空状态检测
std::cout << "\n=== 测试7:空状态检测 ===" << std::endl;
ErrorCollector collector7;
std::cout << "初始Empty: " << (collector7.Empty() ? "true" : "false") << std::endl;
collector7.AddInfo("测试", 100);
std::cout << "添加后Empty: " << (collector7.Empty() ? "true" : "false") << std::endl;
printTestResult("空状态检测", collector7.Empty() == false);
// 测试8:大量数据性能
std::cout << "\n=== 测试8:大量数据性能 ===" << std::endl;
ErrorCollector collector8;
for (int i = 0; i < 1000; i++) {
collector8.AddInfo("错误_" + std::to_string(i), i);
}
std::string result8 = collector8.GetInfos(true);
bool hasError500 = result8.find("错误_500") != std::string::npos;
std::cout << "包含第500条: " << (hasError500 ? "是" : "否") << std::endl;
printTestResult("大量数据处理", hasError500);
std::cout << "\n=== 所有测试完成 ===" << std::endl;
return 0;
}
编译命令:
g++ -std=c++11 main.cpp
执行输出结果:
# ./a.out === 测试1:基本功能 === 所有信息: 数据库连接失败 | 配置文件缺失 | 用户认证超时 仅错误: 数据库连接失败 | 配置文件缺失 错误码: 1001|1002 [PASS] 基本功能 === 测试2:去重功能 === 去重后结果: 重复错误 | 唯一错误 错误码: 100|300 [PASS] 去重功能 === 测试3:空值和特殊字符过滤 === 过滤后结果: 有效消息 [PASS] 空值过滤 [PASS] 空状态检测 === 测试4:特殊字符清理 === 清理后结果: 消息包含制表符 | 第一行第二行 [PASS] 制表符清理 === 测试5:按错误码获取 === 错误码100的信息: 错误A 跳过错误码200: 100|300 [PASS] 按码获取 [PASS] 跳过码获取 === 测试6:输出格式验证 === 格式结果: A | B | C [PASS] 格式正确 === 测试7:空状态检测 === 初始Empty: true 添加后Empty: false [PASS] 空状态检测 === 测试8:大量数据性能 === 包含第500条: 是 [PASS] 大量数据处理 === 所有测试完成 ===
4.2 关键测试用例
用例1:基本功能验证
collector.AddInfo("数据库连接失败", 1001);
collector.AddInfo("配置文件缺失", 1002);
collector.AddInfo("用户认证超时", 0);
用例2:去重验证
collector2.AddInfo("重复错误", 100);
collector2.AddInfo("重复错误", 200); // 相同消息,不同code
collector2.AddInfo("唯一错误", 300);
用例3:特殊字符清理
ErrorCollector collector4;
collector4.AddInfo("消息\t包含\t制表符", 100);
collector4.AddInfo("第一行\n第二行\r\n", 200);
4.3 测试结论
所有测试用例全部通过,验证了实现的正确性和健壮性。
| 测试项 | 预期结果 | 实际结果 | 状态 |
|---|---|---|---|
| 基本添加和获取 | 正确存储和检索 | 通过 | ✅ |
| 消息去重 | 相同消息只保留一个 | 通过 | ✅ |
| 空值过滤 | 空字符串被忽略 | 通过 | ✅ |
| 特殊字符清理 | 移除\t\n\r | 通过 | ✅ |
| 按错误码获取 | 正确过滤 | 通过 | ✅ |
| 跳过指定错误码 | 正确排除 | 通过 | ✅ |
| 输出格式 | 竖线分隔,无多余符号 | 通过 | ✅ |
| 空状态检测 | 状态正确 | 通过 | ✅ |
| 性能测试 | 1000条数据正常处理 | 通过 | ✅ |
5. 其它说明
ErrorCollector虽然是一个简单的工具类,但它在实际项目中发挥了重要作用。以下是一些要点说明和未来改进方向:
- 其它信息收集:
ErrorCollector虽然名为错误收集器,但通过创建多个副本,可实现其它信息的收集。另外,可以根据 - 线程安全:在多线程环境下,需要添加互斥锁保护
- 容量限制:防止错误消息无限增长,可以设置最大容量
- 持久化:支持将错误信息序列化到文件或数据库
6. 小结
错误收集看似简单,但设计一个好用、健壮的错误收集器需要对使用场景有深入理解。ErrorCollector的实践证明,即使是小型工具类,只要设计得当,也能在项目中发挥"四两拨千斤"的效果。
在软件开发中,我们常常追求复杂而全面的解决方案,但有时候,一个简洁、专注的工具可能更符合"够用就好"的工程哲学。正如这个错误收集器所展示的:在正确的抽象层次上,解决正确的问题,就是好的设计。
以上就是使用C++实现的错误信息收集器的详细内容,更多关于C++错误信息收集器的资料请关注脚本之家其它相关文章!
