C++跨平台(十五):跨平台错误处理策略
错误的本质从errno到std::error_codeC程序中的错误来自两个完全不同的世界。一是C标准库和应用程序自身的错误——通常通过异常std::runtime_error或返回值std::optional、std::expected来传递。二是操作系统系统调用的错误——在POSIX上通过errno在Windows上通过GetLastError()。这两个世界的错误表示方式完全不同而跨平台代码需要在它们之间建立桥梁。errno vs GetLastError()POSIX的errno是一个线程局部变量在glibc中通过__thread或等效机制实现C11之后标准化为thread-local。系统调用失败时设置errno标准库提供strerror()和perror()来获取人类可读的描述。Windows的GetLastError()使用线程局部存储TLS槽位存储最后一次Win32 API调用的错误码。用FormatMessage()获取错误描述。关键差异Windows错误码与POSIX的errno值如EACCES、ENOENT通常不一致——它们是两套独立的数字空间。// 跨平台系统错误码获取#ifdef_WIN32#includewindows.h#defineget_system_error()GetLastError()#else#includeerrno.h#defineget_system_error()errno#endif// 获取错误描述std::stringformat_system_error(uint64_terror_code){#ifdef_WIN32char*msg_bufnullptr;FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,nullptr,error_code,MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),(LPSTR)msg_buf,0,nullptr);std::stringmsg(msg_buf?msg_buf:Unknown error);LocalFree(msg_buf);returnmsg;#elsereturnstd::string(strerror(static_castint(error_code)));#endif}std::error_codeC11的统一错误表示C11引入的std::error_code提供了一种平台无关的错误表示。它是一个包含错误码整数值和指向std::error_category引用的轻量级对象两个指针的大小。error_category负责提供错误描述和比较逻辑。标准库定义了两个全局错误类别std::generic_category()对应POSIX errno值和std::system_category()对应操作系统的原生错误码。在POSIX上两者通常等价在Windows上std::system_category()封装了Win32的GetLastError()码。#includesystem_error#includeiostreamvoidcheck_file_error(std::error_code ec){if(!ec)return;// ec.value() 0 表示成功std::cerrError: ec.message()// 人类可读描述 (code: ec.value()// 错误码, category: ec.category().name()// 类别)std::endl;}std::filesystem库中的所有函数都提供了接受std::error_code作为输出参数的重载版本而非抛出异常这为不想使用异常的代码提供了替代路径#includefilesystem#includesystem_errornamespacefsstd::filesystem;// 异常版本fs::create_directory(/path/to/dir);// error_code版本不抛异常std::error_code ec;fs::create_directory(/path/to/dir,ec);if(ec){std::cerrFailed: ec.message()std::endl;}std::expectedC23的返回值革命C23引入了std::expectedT, E——这是一个包含值或错误类型类似于Rust的ResultT, E类型。它比异常更适合可预期的错误路径如文件未找到、网络超时、格式错误因为调用方被迫检查结果#includeexpected#includestringenumclassParseError{InvalidFormat,MissingField,OutOfRange,};std::expectedint,ParseErrorparse_port(conststd::stringstr){try{intportstd::stoi(str);if(port1||port65535){returnstd::unexpected(ParseError::OutOfRange);}returnport;}catch(conststd::invalid_argument){returnstd::unexpected(ParseError::InvalidFormat);}}// 使用方式一检查并解包autoresultparse_port(8080);if(result){std::coutPort: *resultstd::endl;}else{std::cerrParse error: static_castint(result.error());}// 使用方式二and_then 链式调用monadic operationsautofinal_portparse_port(8080).and_then([](intport)-std::expectedint,ParseError{if(port80)return8080;// 重映射特权端口returnport;}).or_else([](ParseError e)-std::expectedint,ParseError{// 出错时使用默认值return3306;}).value_or(3306);// 获取最终值无论是否有错误提供一个后备值std::expected的引入让C有了与Rust的Result类型相对应的、符合现代工程实践的错误处理机制。它相比异常的优势在于调用方无法忽略错误必须显式检查否则编译器会对未使用的[[nodiscard]]返回值发出警告。无运行时开销除了存储error的空间不像异常有展开栈stack unwinding的代价。与std::error_code天然配合std::expectedT, std::error_code是最常见的用法。跨平台的异常安全C异常在不同平台上的实现机制不同。所有主流实现GCC的DWARF2/LSDA、MSVC的SEH、Clang的Itanium C ABI都使用零开销异常模型——正常路径不抛异常没有运行时开销抛出异常时通过查找表LSDA语言特定数据区进行栈展开。但一些边缘行为值得注意在Windows上跨DLL边界抛异常如果DLL和主程序使用不同的编译器或不同的异常设置编译行为是未定义的。特别是用MSVC编译的DLL抛出的异常不能被MinGW编译的主程序捕获反之亦然。跨noexcept边界抛异常在所有平台上都调用std::terminate()。这是C标准规定的行为跨平台一致。析构函数中的异常如果在栈展开过程中抛出即double fault同样调用std::terminate()。从错误码到异常的桥接在跨平台项目中经常需要将平台特定的系统错误转换为统一的异常体系#includesystem_error#includestdexcept// 将系统错误转换为异常voidthrow_system_error(conststd::stringcontext){autoecstd::error_code(#ifdef_WIN32static_castint(GetLastError()),#elseerrno,#endifstd::system_category());throwstd::system_error(ec,context);}// 使用voidopen_config_file(conststd::stringpath){std::ifstreamfile(path);if(!file.is_open()){throw_system_error(Failed to open: path);}// ...}日志级别的跨平台一致性虽然日志本身不是错误处理但好的日志策略能极大帮助跨平台调试。推荐使用经过验证的日志库如spdlog——Header-only跨平台异步支持多种sink它内部已经处理了文件路径、线程ID获取、时间格式化等平台差异。#includespdlog/spdlog.hintmain(){// 跨平台日志配置spdlog::set_pattern([%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] %v);SPDLOG_INFO(Application started);SPDLOG_ERROR(Failed to connect to database: {},ec.message());SPDLOG_DEBUG(Processing request #{},request_id);return0;}spdlog的%t格式说明符使用std::this_thread::get_id()获取线程标识在所有平台上行为一致。%Y-%m-%d等时间格式使用平台无关的格式化。实际建议用std::error_code封装系统错误。不要在整个代码库中散落errno或GetLastError()——在系统调用边界处立即转换为std::error_code后续代码只使用这个跨平台抽象。区分可恢复和不可恢复的错误。可恢复错误用std::expected或std::optional返回不可恢复的编程错误逻辑矛盾、不变量违反用assert或异常。不要跨模块边界抛异常。公开的C API和插件接口使用错误码内部可以使用异常。C23项目优先使用std::expected。它让错误路径显式化、无法被忽略。在CI中测试错误路径。成功的路径在你开发时已经被反复测试但错误路径往往在用户现场才首次触发。使用故障注入或模拟来覆盖错误处理代码。