从std::ifstream读取二进制数据时避免未定义行为(UB)
二进制反序列化代码的未定义行为分析
简化假设
- 文件格式完全正确,包含一个
std::uint32_t、一个std::int64_t和一个float,字节序与程序匹配,且float的内存表示和程序一致 - 仅处理隐式生存期、平凡可复制、平凡析构的类型
原始待审查代码
struct Content { std::uint32_t first; std::int64_t second; float last; }; std::string Path("<Some valid path>"); // 存储目标数据的二进制文件路径 std::ifstream is(Path, std::ios::binary); Content content; is.read(reinterpret_cast<char*>(&content.first), sizeof(content.first)); is.read(reinterpret_cast<char*>(&content.second), sizeof(content.second)); is.read(reinterpret_cast<char*>(&content.last), sizeof(content.last));
核心问题
这段代码里的reinterpret_cast是否合法?是否应该改用以下两种实现?
方案1:memcpy实现
char buffer[sizeof(std::uint32_t)]; is.read(buffer, sizeof(content.first)); std::memcpy(&content.first, buffer, sizeof(content.first)); // 对second、last重复上述逻辑
方案2:bit_cast实现
char buffer[sizeof(std::uint32_t)]; is.read(buffer, sizeof(content.first)); content.first = std::bit_cast<std::uint32_t>(buffer); // 对second、last重复上述逻辑
问题解答
原始代码的合法性
在你给出的假设前提下,这段代码不存在未定义行为,是合法的。
原因是C++标准允许:将任何对象的存储区以字节序列的形式访问(即直接读取/写入对象的"对象表示"),而你处理的都是平凡可复制类型——这类类型的对象表示直接对应其值表示,且隐式生存期类型不需要显式构造即可直接操作其存储区。reinterpret_cast<char*>(&member)本质是把成员对象的指针转换成字节指针,用于读取字节到对象的存储区,完全符合标准规则。
替代方案的对比
- memcpy方案:这是标准明确认可的最安全的序列化/反序列化方式之一,完全没有UB风险。
memcpy的语义就是复制字节序列,对于平凡可复制类型,用它将字节写入对象存储区是标准允许的初始化/赋值操作,兼容性极佳(支持所有C++版本)。 - bit_cast方案:C20引入的标准工具,要求源和目标类型的大小完全相同,且均为平凡可复制类型。它会直接将源的字节表示转换为目标类型的值,编译期就能检查大小匹配问题,类型安全性更高,代码也更简洁,是现代C中推荐的做法。
总结
原始代码在你的假设下是合法的,但memcpy和bit_cast是更稳妥、更符合现代C++最佳实践的选择,尤其是bit_cast能在编译期排查错误,优先推荐使用。
内容的提问来源于stack exchange,提问作者Oersted
相关产品推荐
相关产品推荐

