如何让value_type构造函数向解析函数标识非致命异常?
我正在开发一个需读取10余种不同类型CSV文件的应用,数据将存入std::map或vector等容器。此前每种文件都有独立解析函数,我正将其统一为单个模板函数,以降低后续维护成本并实现统一的损坏文件错误报告。
该函数逐行读取内容,判断容器是否具备key概念(如std::map),对关联容器使用emplace方法,对其他容器(如vector)使用emplace_back方法。容器的value_type类仅需满足能通过CSV行实例化的要求,其构造函数抛出的任何异常都被视为致命错误,程序会报告输入文件名和行号后退出,代码如下:
try { if constexpr (is_associative_container<Container>(NULL)) { result->emplace(typename Container::key_type( key, keylen), value); } else { result->emplace_back(value); } } catch (const std::exception &e) { fprintf(stderr, "%s:%zd: %s\n", path, line, e.what()); goto failure; }
目前该函数已完成约75%的CSV解析工作,但剩余25%的CSV文件处理更复杂:其中部分行需特殊处理,内容不应存入容器。我想知道,value_type的构造函数如何向解析函数标识抛出的异常不应被视为致命异常?使用标准异常(如std::bad_function_call)作为标识的方式不可靠,因为无法保证该异常不会意外出现,还有其他解决方案吗?
1. 定义专属的非致命异常类
直接自定义一个用于标记“需跳过该行”的异常类型,彻底区分致命错误和跳过行的场景:
// 自定义异常,仅用于标记当前行无需存入容器 class SkipRowException : public std::exception { public: const char* what() const noexcept override { return "Row should be skipped"; } };
修改解析函数的catch逻辑,单独处理该异常:
try { if constexpr (is_associative_container<Container>(NULL)) { result->emplace(typename Container::key_type(key, keylen), value); } else { result->emplace_back(value); } } catch (const SkipRowException&) { // 仅跳过当前行,不触发致命错误流程 continue; } catch (const std::exception &e) { fprintf(stderr, "%s:%zd: %s\n", path, line, e.what()); goto failure; }
在value_type构造函数中,遇到需要跳过的行时直接抛出SkipRowException即可,完全不会和标准异常或其他致命异常混淆。
2. 拆分构造与行过滤逻辑,用可选值替代异常
如果不想用异常控制流程,可以把“行是否保留”的判断从构造函数中拆分出来,为value_type添加静态工厂方法返回std::optional<value_type>:
struct MyValueType { // 私有构造函数,仅通过工厂方法创建 MyValueType(/* 构造参数 */) { /* ... */ } static std::optional<MyValueType> from_csv_row(const std::string& row) { if (/* 判断该行需要跳过,比如是注释、特殊标记行 */) { return std::nullopt; } // 正常构造对象,构造失败则抛出致命异常 try { return MyValueType(/* 解析row得到的参数 */); } catch (...) { throw; } } };
解析模板函数中先通过工厂方法判断是否保留该行:
// 逐行读取逻辑中... std::string current_row; while (getline(file, current_row)) { line++; auto opt_value = ValueType::from_csv_row(current_row); if (!opt_value) { continue; } // 插入容器的逻辑 try { if constexpr (is_associative_container<Container>(NULL)) { result->emplace(typename Container::key_type(key, keylen), *opt_value); } else { result->emplace_back(*opt_value); } } catch (const std::exception &e) { fprintf(stderr, "%s:%zd: %s\n", path, line, e.what()); goto failure; } }
这种方式把“跳过行”的逻辑从错误异常中分离,更符合异常仅用于错误场景的设计原则。
3. 为模板函数添加行过滤回调
如果不同CSV的行过滤规则差异较大,可以给模板解析函数添加可选回调参数,由调用方决定行的保留规则:
template<typename Container> void parse_csv(const std::string& path, Container* result, std::function<bool(const std::string&)> should_keep_row = [](const auto&){return true;}) { // 打开文件等前置逻辑... std::string current_row; size_t line = 0; while (getline(file, current_row)) { line++; if (!should_keep_row(current_row)) { continue; } // 后续解析、插入容器的逻辑... } }
调用时针对特殊CSV传入过滤规则:
std::vector<MyValueType> vec; parse_csv("special.csv", &vec, [](const std::string& row) { // 示例:跳过注释行和空行 return !row.starts_with("#") && !row.empty(); });
这种方式让解析函数只负责通用的解析和插入逻辑,过滤逻辑完全由调用方控制,职责更清晰。
内容的提问来源于stack exchange,提问作者Mikhail T.

