CppCon 2021 学习: Failing Successfully
内容要点:理解:理解:理解:总结:解析器需要提供错误上下文(指针、位置、错误类型)设计方式:返回并通过输出错误信息提供额外方法:→ 返回格式化错误字符串→ 提供解析位置指针:→ 枚举错误来源 (, , , , , )提供接口访问消息解析器总结:解析器设计中,错误通过返回值 ++ 上下文方法的组合报告避免直接抛出异常,也不丢失位置信息和错误来源4. 多线程错误处理(第 28-30 页)问题:多线程环
·
内容要点:
- 程序终止的方式有好坏
- 正确结构化的终止:比如通过函数返回值或异常机制,让调用者能够知道发生了什么并处理。
exit
之类的函数:直接终止程序,绕过调用栈,不让调用者做处理。
exit
的问题- 隐藏控制流(Hidden control flow)
goto
使得函数内部的控制流难以理解。exit
使得函数间的控制流难以理解:函数内部直接终止程序,调用者无法知道发生了什么。
- 全局状态(Global state)
- 通常认为全局状态指的是全局变量,但更广义上,还包括程序的非局部影响(比如文件、数据库、网络状态)。
- 直接
exit
会导致非局部状态改变被“悄悄”触发,难以追踪。
总结:exit
虽然简单,但会破坏程序结构、可维护性和可预测性,不建议在库或可重用代码中随意使用。
内容要点:
- 隐藏控制流(Hidden control flow)
- 不要把多个不相关的决策耦合在一起
- 遵循单一职责原则(Single-Responsibility Principle)。
- 耦合决策会降低代码复用能力。
- 不要在代码中“污染”非局部关注点(non-local concerns)
- 例如:一个函数的错误导致程序直接退出,这把终止责任强行加给了函数本身,而终止本应该由更高层的上下文决定。
- 终止程序不是局部责任
- 决策应该延迟到程序有足够上下文可以作出适当决定时再执行(例如在顶层的 main 函数或者错误处理框架)。
总结:把程序终止责任放到局部函数,会破坏模块化设计。应该将错误处理和终止决策交给更高层次的上下文。
内容要点:
- 决策应该延迟到程序有足够上下文可以作出适当决定时再执行(例如在顶层的 main 函数或者错误处理框架)。
- 以
ENOENT
(文件或目录不存在)为例,错误信息本身是标准的,但如果缺少上下文(比如哪个路径不存在),就不够有用。 - 上下文丢失问题
- 调用
open
时,路径信息是可用的,但某些层级在传递过程中丢掉了这个信息。 - 原因可能有:
- 结构性原因:程序没有设计好保存错误上下文的机制。
- 功能性原因:程序可用的机制没有正确传递或保存上下文信息。
总结:错误信息要有上下文,否则用户无法理解或定位问题。
内容要点:
- 调用
int atoi(const char* str);
设计只针对成功情况:- 输入合法数字字符串 → 返回整数
- 问题:如果字符串不是整数
atoi
返回 0,但字符串也可能表示数字 0- 不能区分“解析失败”与“解析结果为 0”
- 假设输入总是合法数字,这是不安全的设计
- 隐患:忽略尾部非数字字符,也没有错误报告机制。
总结:设计仅考虑成功情况的 API 会导致错误被默默忽略,缺乏健壮性。
整数解析、错误报告机制、以及现代 C++ 的 std::error_code
/std::error_condition
。
optional<int> real_atoi(const char* str) noexcept {
const auto result = std::atoi(str);
if (result) {
return result;
}
while (*str && std::isspace(static_cast<unsigned char>(*str))) {
++str;
}
if (*str == '-') { ++str; }
if (*str == '0') { return 0; }
return nullopt;
}
理解:
- 这是对
atoi
的改进封装,返回optional<int>
,用来明确表示 解析失败。 - 逻辑:
- 调用
std::atoi
。 - 如果结果不为 0,返回结果。
- 如果结果为 0,手动检查字符串:
- 跳过空格
- 跳过负号
- 如果剩下的是
'0'
→ 返回 0 - 否则返回
nullopt
→ 表示解析失败
- 调用
- 目的:解决
atoi
的问题,即不能区分 “输入非整数” 和 “输入是数字 0”。
11 页 — strtol
long strtol(const char* str, char** str_end, int base);
理解:
strtol
更好地支持错误检测:- 成功:
*str_end
指向解析到的最后一个字符之后 - 失败:
*str_end
指向输入字符串的开头
- 成功:
- 返回值仍可能为 0(所以仅凭返回值无法判断成功/失败)
- 可以区分成功与失败,但无法轻易区分 不同类型的错误 或 错误位置。
12 页 — from_chars
struct from_chars_result {
const char* ptr;
errc ec;
};
from_chars_result from_chars(const char* first, const char* last, T& value, int base = 10);
理解:
from_chars
是 C++17 提供的整数/浮点解析机制,显式返回错误信息ec
(error code)可区分:- 溢出
- 非整数字符串
ptr
指向解析停止的位置
- 局限:
ptr
在失败时指向first
,无法精确指出失败发生的具体位置。
13 页 — Fail Fast, Fail Often
- 上述解析函数通常忽略 前导空格
- 调用者可能希望前导空格视为错误
- 如果解析函数忽略空格,就等于 替用户做了决策 → 减少灵活性
- 建议:调用者自己跳过空格,或者严格处理空格。
14 页 — Error Vocabulary
- 传统 C 风格错误报告:
- 用
int
或枚举表示错误(如errno
、CURLcode
) - 在单一调用下可用,但 组合调用时容易丢失上下文
- 用
- 例如:一个函数依次调用 POSIX 与 libcurl 函数 → 如何传递和组合错误信息?
15 页 — std::error_code
- 将 整数错误码 与 错误类别(category) 结合
- category 决定错误码如何解释
- 同样的数值,不同 category 表示不同错误
- category 是
std::error_category
的单例 - 指针身份唯一性:通过指针判断 category 是否相等
16 页 — Error Handling 示例
- 文件不存在:
errno
→ENOENT
CURLcode
→CURLE_FILE_COULDNT_READ_FILE
- C 风格只能对少数已知值做判断
std::error_code
可理论上处理无限多的错误类型和组合
17-18 页 — std::error_condition
与自定义错误类别
std::error_condition
:用于表示根本原因,可编程处理- 可与
std::error_code
比较 - 自定义
std::error_category
:- 提供
name()
、message()
、default_error_condition()
equivalent
用于判断错误码是否相当
- 提供
- 示例:自定义 decimal parser 错误枚举:
enum class error { success = 0, bad_whole, no_decimal, bad_decimal };
std::error_code make_error_code(error e) noexcept {
static const struct : std::error_category { ... } category;
return std::error_code(static_cast<int>(e), category);
}
namespace std {
template<>
struct is_error_code_enum<error> : true_type {};
}
总结:
std::error_code
+std::error_category
提供可组合、可扩展、类型安全的错误报告机制- 自定义 error 枚举可以直接与标准错误体系兼容
std::system_error
、异常处理设计、FIX 消息解析、以及多线程错误处理。
1. std::system_error
- 定义:
std::system_error
是一个异常类型,封装了一个std::error_code
。
- 用途:
- 可以继承并重载
what()
,附加上下文信息。 - 调用栈上可以捕获
std::system_error
并处理,也可以捕获std::exception
并打印what()
。
- 可以继承并重载
- 前提:使用此类型意味着你的设计是允许抛出异常的。
2. 异常的使用原则
- 常见观点:异常用于“特殊情况(exceptional situations)”
- 问题:认为某些事情“特殊”,等于替用户做决策
- 优点:
- 错误报告简单:直接
throw
- 上下文传播方便:可在异常类型中添加额外信息,自定义
what()
- 错误报告简单:直接
- 缺点:
- 错误处理复杂:不知道应该
catch
哪个异常 - 代码分析困难:不清楚哪些操作会失败以及如何失败
- 错误处理复杂:不知道应该
- 结论:构建块越高层,使用异常越合理
3. FIX 消息解析
- 问题示例:
8=FIX.4.2\x019=00238\x0135=D\x0134=160\x0149=P98004N\x015a=004\x0152=2
^
Tag could not be parsed as an integer
- 解析器需要提供错误上下文(指针、位置、错误类型)
- 设计方式:
fix_message_reader
返回fix_message*
并通过std::error_code& ec
输出错误信息- 提供额外方法:
format_last_error()
→ 返回格式化错误字符串last() / last_begin() / last_end() / begin() / end()
→ 提供解析位置指针
standard_fix_client
:last_error_source()
→ 枚举错误来源 (parsable
,verify
,parse_fix
,parse_unknown
,stop
,other
)- 提供
message_reader()
接口访问消息解析器
总结:
- 解析器设计中,错误通过返回值 +
std::error_code
+ 上下文方法的组合报告 - 避免直接抛出异常,也不丢失位置信息和错误来源
4. 多线程错误处理(第 28-30 页)
- 问题:
- 多线程环境中,“通过返回值报告错误”不再合理
- 必须能够:
- 收集所有线程的错误
- 在任意线程发生错误时停止线程池
- 实现方式:
thread_pool
类示例
class thread_pool {
struct state : ::asio::io_context { std::thread thread; };
std::list<state> states_;
mutable std::mutex m_;
std::exception_ptr ex_; // 捕获异常
public:
explicit thread_pool(unsigned threads);
void run();
void stop(std::exception_ptr ex = std::exception_ptr()) noexcept;
};
- 运行线程池:
void thread_pool::run() {
const auto run = [&](auto&& ctx) noexcept {
try {
ctx.run();
} catch (...) {
stop(std::current_exception());
}
};
auto begin = std::next(states_.begin(), 1);
const auto g = make_scope_exit([&]() noexcept {
for (auto iter = std::next(states_.begin(), 1); iter != begin; ++iter) {
iter->stop();
iter->thread.join();
}
});
for (const auto end = states_.end(); begin != end; ++begin) {
begin->thread = std::thread([&, begin]() noexcept { run(*begin); });
}
run(states_.front());
const std::lock_guard g(m_);
if (ex_) std::rethrow_exception(std::move(ex_));
}
理解:
- 每个线程在执行时捕获异常并通过
stop(std::current_exception())
传递 - 使用
std::exception_ptr
存储异常,保证线程安全收集 - 最后在主线程中重新抛出异常,统一处理
- 这种模式可以:
- 收集多线程异常
- 停止所有线程
- 保持异常信息
整体总结
std::system_error
:用于异常化错误报告,封装std::error_code
- 异常设计原则:异常可简化错误传播,但会复杂化处理和分析
- FIX 消息解析:错误通过返回值 +
std::error_code
+ 上下文方法报告 - 多线程错误处理:使用
std::exception_ptr
收集线程异常,保证统一停止与处理
错误的主观性、成功/失败的意义、事件回调设计、以及警告/日志策略。
1. 错误是谁的?(Whose Error?,第 33 页)
- 错误的判定依赖于上下文:
- 抽象层次(Level of abstraction):
read
操作通常不把到达文件末尾(EOF)当作错误。- 但是,如果目标是填充缓冲区,EOF 可能被当作错误。
- 在连接管理中,流结束可能不被视为错误。
- 目的(Purpose):
- 无效 XML → 解析 XML 时是错误
- 同时,用来猜测文件是否可能是 XML → 不是错误
总结:错误不是绝对的,而是与上下文和意图相关。
- 抽象层次(Level of abstraction):
2. 成功、失败,谁在乎?(Succeed, Fail, Who Cares?,第 34 页)
- 例子:TCP 连接
- 对客户端来说,成功可能意味着“收到 goodbye 消息”或“优雅关闭”
- 对服务器来说,连接丢失本身就是结果,不管成功/失败细节
- 成功与失败在服务器的处理方式可能是一样的 → 结果 vs. 过程的区别
- 设计原则:不要让错误或成功判定影响整体流程,尤其在高层系统中。
3. Processor Manager 回调设计(第 34-38 页)
- 核心类:
struct processor_manager {
explicit processor_manager(const processor_manager_settings& settings);
void add_device(device& d);
void add_feed(feed& f);
void start();
void stop() noexcept;
void subscribe(processor_manager_callback& callback);
};
- 回调事件:
struct processor_manager_callback {
virtual void on(const device_processor_begin& e) = 0;
virtual void on(const packet_processor_begin& e) = 0;
virtual void on(const device_processor_end& e) = 0;
virtual void on(const packet_processor_end& e) = 0;
};
- 事件参数:
device_processor_end
:包含std::error_code ec
、std::exception_ptr ex
、以及处理对象device* which
packet_processor_end
:包含std::exception_ptr ex
、以及处理对象session* which
- 扩展示例:
eof_processor_manager_callback
- 提供
processor source()
、name()
、wait()
、eof()
、maybe_throw()
等方法 - 用于报告 EOF 或其他终止状态
总结:
- 提供
- 回调机制将 处理开始/结束、成功/失败 抽象出来
- 允许调用者决定如何处理错误,而不是硬编码行为
4. 警告与日志(Warnings & Logging,第 40 页)
- 特性:
- 是带外通信(Out of band communication)
- 可以在成功时发出警告,失败时记录日志
- 注意:
- 日志不应作为长期替代错误报告的手段
- 日志不应耦合到组件内部 → 应通过事件发布
- 消费事件的独立组件负责写日志
总结:
- 警告和日志应作为独立的渠道,用事件驱动方式处理
- 避免组件内部耦合,保持模块化
5. 总结(第 41 页)
- 不要:
- 假设失败不会发生
- 代替用户做不必要的决策
- 丢弃潜在有用的上下文
核心原则:
- 错误处理应透明、可组合、可传递上下文
- 成功/失败应根据上下文定义,不要在系统内部随意判定
- 警告和日志应独立处理,而不是混入业务逻辑
完整的 C++ 示例,把你前面几页讲的错误处理原则、整数解析、std::error_code
、异常、回调、日志、多线程都结合起来,形成一个可编译的示例。这个示例是一个简化版的“设备消息处理器”,包括:
- 自定义整数解析
from_chars
+ 错误码 processor_manager
回调机制- 多线程处理
- 错误/异常收集
- 警告/日志事件机制
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <list>
#include <optional>
#include <system_error>
#include <exception>
#include <charconv>
#include <functional>
// ===========================
// 1. 自定义整数解析
// ===========================
enum class parse_error { success = 0, not_integer, overflow };
struct parse_category : std::error_category {
const char* name() const noexcept override { return "parse_category"; }
std::string message(int code) const override {
switch (static_cast<parse_error>(code)) {
case parse_error::success: return "Success";
case parse_error::not_integer: return "Not an integer";
case parse_error::overflow: return "Overflow";
}
return "Unknown";
}
};
inline std::error_code make_error_code(parse_error e) noexcept {
static parse_category category;
return std::error_code(static_cast<int>(e), category);
}
namespace std {
template <>
struct is_error_code_enum<parse_error> : true_type {};
}
std::optional<int> safe_atoi(const std::string& str, std::error_code& ec) noexcept {
int value{};
auto result = std::from_chars(str.data(), str.data() + str.size(), value);
if (result.ec == std::errc()) {
ec = parse_error::success;
return value;
}
ec = parse_error::not_integer;
return std::nullopt;
}
// ===========================
// 2. Processor 回调机制
// ===========================
struct device_processor {};
struct packet_processor {};
struct device_processor_begin { device_processor& processor; };
struct packet_processor_begin { packet_processor& processor; };
struct device_processor_end : device_processor_begin {
std::error_code ec;
std::exception_ptr ex;
device_processor* which;
device_processor_end(device_processor& p) : device_processor_begin{p}, which(&p) {}
};
struct packet_processor_end : packet_processor_begin {
std::exception_ptr ex;
packet_processor* which;
packet_processor_end(packet_processor& p) : packet_processor_begin{p}, which(&p) {}
};
struct processor_manager_callback {
virtual void on(const device_processor_begin& e) = 0;
virtual void on(const packet_processor_begin& e) = 0;
virtual void on(const device_processor_end& e) = 0;
virtual void on(const packet_processor_end& e) = 0;
};
// ===========================
// 3. Processor Manager
// ===========================
struct processor_manager_settings {};
struct processor_manager {
std::vector<device_processor> devices;
std::vector<packet_processor> packets;
processor_manager_callback* cb{nullptr};
explicit processor_manager(const processor_manager_settings&) {}
void add_device(device_processor& d) { devices.push_back(d); }
void add_feed(packet_processor& p) { packets.push_back(p); }
void start() {}
void stop() noexcept {}
void subscribe(processor_manager_callback& callback) { cb = &callback; }
void process() {
if (!cb) return;
// 模拟设备处理
for (auto& d : devices) {
cb->on(device_processor_begin{d});
device_processor_end e(d);
try {
// 模拟可能的错误
std::error_code ec;
auto v = safe_atoi("abc", ec);
e.ec = ec;
if (!v) throw std::system_error(ec, "Device parse error");
} catch (...) {
e.ex = std::current_exception();
}
cb->on(e);
}
// 模拟 packet 处理
for (auto& p : packets) {
cb->on(packet_processor_begin{p});
packet_processor_end e(p);
try {
// 模拟处理
} catch (...) {
e.ex = std::current_exception();
}
cb->on(e);
}
}
};
// ===========================
// 4. Logging / Callback 实现
// ===========================
struct logging_callback : processor_manager_callback {
void on(const device_processor_begin& e) override {
std::cout << "[LOG] Device begin\n";
}
void on(const packet_processor_begin& e) override {
std::cout << "[LOG] Packet begin\n";
}
void on(const device_processor_end& e) override {
std::cout << "[LOG] Device end, error: " << e.ec.message() << "\n";
if (e.ex) {
try { std::rethrow_exception(e.ex); }
catch (const std::system_error& se) { std::cout << "Exception: " << se.what() << "\n"; }
}
}
void on(const packet_processor_end& e) override {
std::cout << "[LOG] Packet end\n";
if (e.ex) { try { std::rethrow_exception(e.ex); } catch(...) {} }
}
};
// ===========================
// 5. Multi-threaded example
// ===========================
struct thread_pool {
std::list<std::thread> threads;
std::mutex m_;
std::exception_ptr ex_;
template <typename Func>
void run(Func f, unsigned count) {
for (unsigned i = 0; i < count; ++i) {
threads.emplace_back([&, i](){
try { f(i); }
catch (...) {
std::lock_guard<std::mutex> g(m_);
ex_ = std::current_exception();
}
});
}
for (auto& t : threads) t.join();
if (ex_) std::rethrow_exception(ex_);
}
};
// ===========================
// 6. Main
// ===========================
int main() {
processor_manager_settings settings;
processor_manager pm(settings);
device_processor d1;
packet_processor p1;
pm.add_device(d1);
pm.add_feed(p1);
logging_callback cb;
pm.subscribe(cb);
std::cout << "=== Single-threaded processing ===\n";
pm.process();
std::cout << "\n=== Multi-threaded processing ===\n";
thread_pool pool;
pool.run([&](unsigned id){ pm.process(); }, 2);
return 0;
}
示例特点总结
- 整数解析:
- 使用
std::from_chars
+std::error_code
返回明确错误 - 可区分非整数、溢出等情况
- 使用
- Processor Manager 回调:
- 处理开始/结束事件
- 将错误信息通过
std::error_code
和std::exception_ptr
传递
- 异常处理:
- 使用
std::system_error
包装std::error_code
- 回调可以选择捕获并打印
- 使用
- 日志/警告机制:
- 日志通过
processor_manager_callback
独立处理 - 与业务逻辑解耦
- 日志通过
- 多线程:
- 线程异常通过
std::exception_ptr
收集 - 主线程统一重新抛出
这个示例已经把你之前讲的整数解析、错误码、异常、回调、多线程、日志都整合起来,是一个完整端到端的实现。
- 线程异常通过
=== Single-threaded processing ===
[LOG] Device begin
[LOG] Device end, error: Not an integer
Exception: Device parse error: Not an integer
[LOG] Packet begin
[LOG] Packet end
=== Multi-threaded processing ===
[LOG] Device begin
[LOG] Device end, error: Not an integer
Exception: Device parse error: Not an integer
[LOG] Packet begin
[LOG] Packet end
[LOG] Device begin
[LOG] Device end, error: Not an integer
Exception: Device parse error: Not an integer
[LOG] Packet begin
[LOG] Packet end
更多推荐
所有评论(0)