C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++错误信息收集器

使用C++实现的错误信息收集器

作者:李迟

还在为C++项目中错误信息碎片化、重复记录和特殊字符干扰而烦恼吗,本文带你深入剖析一个轻量级ErrorCollector类的设计与实现,涵盖去重存储、字符清洗和灵活查询等核心功能,让你的错误处理更高效、代码更简洁,需要的朋友可以参考下

1. 背景

在大型C++项目的开发过程中,错误处理是一个不容忽视的核心环节。年初笔者接手了一个工程,近几个月在修改,发现其中的错误信息处理比较凌乱,考虑到方便日后的自己的维护,因此要进行必要的重构。缺乏统一错误收集机制带来以下一些困扰:

为了解决这些问题,笔者着手设计并实现了一个轻量级的ErrorCollector类。本文将从需求分析、设计思路、实现细节到测试验证,完整分享这个组件的开发过程。

PS:近年来,笔者基本都会因其他共事的人升职、转岗等原因接手不同性质代码工程,但还没能力向他人移交工程或事务。从技术角度看,需要写不同类别的代码,是件好事,但从个人成长角度看,不是个事,特别是处于年龄越来越大,头发越来越白的尴尬的人生阶段。

2. 设计思路

2.1 核心需求

经过需求梳理,我们确定了以下核心功能点:

  1. 去重存储:相同的错误消息只保留一份,避免冗余
  2. 分类标记:每条消息关联一个错误码(0表示普通信息,非0表示错误)
  3. 字符清洗:自动移除\t\n\r等特殊字符
  4. 灵活查询:支持按类型(全部/仅错误)、按错误码过滤查询
  5. 空值安全:对空字符串和空指针有防护
  6. 简单易用:提供直观的接口,且只有头文件,减少使用成本

2.2 数据结构设计

采用两个并行的std::vector分别存储错误码和消息文本:

std::vector<int> m_code;          // 错误码列表
std::vector<std::string> m_list;  // 消息列表(与m_code一一对应)

这种设计的优势有以下几点:

去重机制则通过std::findm_list中线性查找实现。虽然时间复杂度为O(n),但考虑到错误收集场景下消息数量通常有限(一般不超过几十条),这种实现足够高效且代码简洁。

2.3 接口设计原则

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在以下场景中特别有用:

  1. API参数校验:收集所有校验失败的信息,一次性返回给调用方
  2. 批量操作:执行多个独立操作时,收集所有异常而非遇到第一个错误就中断
  3. 配置加载:加载配置文件时,记录所有格式错误或缺失的配置项
  4. 单元测试:收集测试过程中的断言失败信息

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虽然是一个简单的工具类,但它在实际项目中发挥了重要作用。以下是一些要点说明和未来改进方向:

  1. 其它信息收集ErrorCollector虽然名为错误收集器,但通过创建多个副本,可实现其它信息的收集。另外,可以根据
  2. 线程安全:在多线程环境下,需要添加互斥锁保护
  3. 容量限制:防止错误消息无限增长,可以设置最大容量
  4. 持久化:支持将错误信息序列化到文件或数据库

6. 小结

错误收集看似简单,但设计一个好用、健壮的错误收集器需要对使用场景有深入理解。ErrorCollector的实践证明,即使是小型工具类,只要设计得当,也能在项目中发挥"四两拨千斤"的效果。

在软件开发中,我们常常追求复杂而全面的解决方案,但有时候,一个简洁、专注的工具可能更符合"够用就好"的工程哲学。正如这个错误收集器所展示的:在正确的抽象层次上,解决正确的问题,就是好的设计

以上就是使用C++实现的错误信息收集器的详细内容,更多关于C++错误信息收集器的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文