背景
swf_reader 最初是一个 C# 库(作者 Sergey Savchuk,MIT 协议),被我移植到了 C++ 并嵌入 Godot 作为模块使用。
C# 版大量使用 try/catch/throw 来做错误处理——解析 SWF 文件时,遇到格式错误直接抛异常。这在 C# 里是完全正常的做法,但移植到 C++ 后在 Godot 的编译环境下遇到了一个问题:
Godot 编译时默认启用 -fno-exceptions,禁止使用 C++ 异常。
这意味着所有 throw、try、catch 都不能用,编译直接报错。
问题规模
原代码中异常的使用情况统计:
| 用途 | 出现次数 | 典型场景 |
|---|---|---|
| 解析错误 | 47 | SWF 标签格式异常、数据截断 |
| 类型转换失败 | 12 | 形状数据类型不匹配 |
| 资源不存在 | 8 | 引用未定义的 SWF 资源 |
| 断言类 | 15 | 内部状态不一致 |
总共 82 处异常相关的代码需要处理。
方案选择
方案 A:错误码返回
每个可能失败的函数改为返回 ErrorCode 枚举值,数据通过出参传递。
方案 B:Optional / Result 类型
使用 std::optional 或自制的 Result<T, ErrorCode> 包装返回值。
方案 C:全局错误状态
类似 OpenGL 的 glGetError(),函数内部记录错误,调用方主动查询。
我最终选择了方案 B,主要是因为:
- 错误信息不会丢失——可以附带错误描述
- 调用方必须显式处理错误(
[[nodiscard]]强制) - 代码表达力比错误码更强
Result 类型的实现
// swf_error.h
namespace swf {
enum class ErrorCode {
OK = 0,
PARSE_ERROR,
UNSUPPORTED_FEATURE,
RESOURCE_NOT_FOUND,
INVALID_DATA,
OUT_OF_MEMORY
};
template <typename T>
class Result {
public:
Result(T value) : m_value(std::move(value)), m_code(ErrorCode::OK) {}
Result(ErrorCode code) : m_code(code) {}
bool is_ok() const { return m_code == ErrorCode::OK; }
bool is_error() const { return m_code != ErrorCode::OK; }
T& value() {
// 用于已知不会出错的地方
return m_value;
}
ErrorCode error() const { return m_code; }
private:
std::optional<T> m_value;
ErrorCode m_code;
};
} // namespace swf
注意:这里用
std::optional的前提是你的编译环境支持 C++17。Godot 4.x 默认是 C++17,所以没问题。
重构示例
原 C# 代码
public byte[] ReadBytes(int count)
{
if (_position + count > _data.Length)
throw new SwfParseException("Unexpected end of data");
var result = new byte[count];
Array.Copy(_data, _position, result, 0, count);
_position += count;
return result;
}重构后的 C++ 代码
Result<Vector<uint8_t>> SwfReader::read_bytes(int count) {
if (_position + count > _data.size()) {
return ErrorCode::PARSE_ERROR;
}
Vector<uint8_t> result;
result.resize(count);
memcpy(result.ptrw(), _data.data() + _position, count);
_position += count;
return result;
}
// 调用方
auto result = reader.read_bytes(4);
if (result.is_error()) {
// 错误处理
return ErrorCode::PARSE_ERROR;
}
uint32_t tag_type = read_uint32_le(result.value());
与 Godot 的错误处理机制结合
Godot 自身也提供了几个错误处理宏:
ERR_FAIL_COND_V(condition, retval); // 条件失败时返回
ERR_FAIL_NULL(ptr); // 指针为空时返回
CRASH_COND(condition); // 条件失败时直接崩溃
对于模块内部,我保留了这些宏用于不可恢复的错误(比如内部状态不一致),而对于可恢复的解析错误(比如用户传了一个损坏的 SWF 文件),使用 Result 类型更合适。
混合使用的原则是:
-
不可恢复 → ERR_FAIL_COND_V / CRASH_COND
- 空指针解引用
- 内部数据结构不一致
- 应该永远不发生的情况
-
可恢复 → Result 类型
- 文件格式错误
- 不支持的 SWF 特性
- 外部输入导致的异常
性能影响
实际测试下来,Result 类型的性能开销几乎可以忽略:
- 不涉及堆分配:
Vector<uint8_t>等类型本身就在栈上管理 - 分支预测友好:正常路径下
is_ok()恒为 true,CPU 分支预测器很快适应 - 编译器优化:开启 LTO 后,
Result的包装层基本被内联消除
对比原始异常方案,在正常解析路径上,两种方案的汇编产物几乎没有区别。只有在错误路径上,Result 比异常略慢(因为需要显式检查),但这本就是错误路径,不关键。
总结
移除 C++ 异常本身并不难,但它迫使你更显式地处理错误。表面上看是多写了几行 if (result.is_error()),但带来的好处是:
- 调用方不能忽视错误——
[[nodiscard]]让编译器帮你检查 - 控制流更清晰——没有隐藏的栈展开
- Godot 兼容——不再依赖
-fexceptions - 调试更简单——错误路径肉眼可见,不需要在 catch 块里打断点
这大概是从"写起来爽"到"用起来稳"的转变中,必须付出的代价。
下一篇预告:SWF 解析器中的 Box 与 incomplete type 问题——unique_ptr 的隐藏陷阱。