C++: 简化二进制流






4.85/5 (21投票s)
支持字节序交换的简化二进制流
目录
引言
不少 C++ 开发人员习惯了文本流上的 << 和 >> 运算符,但在二进制流上却错过了它们。简化的二进制流只不过是 STL fstream 的 read
和 write
函数的简单包装。读者可能会将其与其他序列化库(如 Boost 序列化库和 MFC 序列化)进行比较,因为它们似乎具有相似的 <<, >> 重载。简化的二进制流不是序列化库:它不处理版本控制、向后/向前兼容性、字节序正确性,而是将所有内容留给开发人员。每个过去使用过 Boost 序列化的开发人员都记忆犹新,当 1.42-1.44 版本的文件被较新版本渲染为不可读时,他们都深受其害。使用序列化库就像将您的文件格式置于不受您控制的第三方之下。虽然简化的二进制流不提供任何序列化便利,但它让开发人员能够控制他们的文件格式。
建议任何使用该库来读取/写入文件格式的人员在其之上实现另一层。在本文中,我们将在查看源代码之前先了解用法。简化的二进制流有两种风格:文件流和内存流。文件流封装了 STL fstream
,而内存流使用 STL vector<char>
将数据保存在内存中。开发人员可以使用内存流来解析从网络下载的文件。
简单示例
写入然后读取的示例与内存流和文件流类似,只是我们在使用输入流读取之前,需要刷新并关闭输出文件流。
#include <iostream>
#include "MiniBinStream.h"
void TestMem()
{
simple::mem_ostream out;
out << 23 << 24 << "Hello world!";
simple::mem_istream in(out.get_internal_vec());
int num1 = 0, num2 = 0;
std::string str;
in >> num1 >> num2 >> str;
cout << num1 << "," << num2 << "," << str << endl;
}
void TestFile()
{
simple::file_ostream out("file.bin", std::ios_base::out | std::ios_base::binary);
out << 23 << 24 << "Hello world!";
out.flush();
out.close();
simple::file_istream in("file.bin", std::ios_base::in | std::ios_base::binary);
int num1 = 0, num2 = 0;
std::string str;
in >> num1 >> num2 >> str;
cout << num1 << "," << num2 << "," << str << endl;
}
两者的输出是相同的
23,24,Hello world!
重载运算符
假设我们有一个 Product
结构体。我们可以像下面这样重载它们
#include <vector>
#include <string>
#include "MiniBinStream.h"
struct Product
{
Product() : product_name(""), price(0.0f), qty(0) {}
Product(const std::string& name,
float _price, int _qty) : product_name(name), price(_price), qty(_qty) {}
std::string product_name;
float price;
int qty;
};
simple::mem_istream& operator >> (simple::mem_istream& istm, Product& val)
{
return istm >> val.product_name >> val.price >> val.qty;
}
simple::file_istream& operator >> (simple::file_istream& istm, Product& val)
{
return istm >> val.product_name >> val.price >> val.qty;
}
simple::mem_ostream& operator << (simple::mem_ostream& ostm, const Product& val)
{
return ostm << val.product_name << val.price << val.qty;
}
simple::file_ostream& operator << (simple::file_ostream& ostm, const Product& val)
{
return ostm << val.product_name << val.price << val.qty;
}
如果 struct
只包含基本类型,并且开发人员可以像下面这样打包 struct
成员而无需填充或对齐,那么他/她可以一次性写入/读取整个 struct
,而不是逐个处理成员。读者应该注意到,我们使用相同的代码重载内存流和文件流。这是不幸的,因为这两种类型的流不是派生自同一个基类。即使它们是,它也无法工作,因为 write
和 read
函数是模板函数,而模板函数不能是虚拟的,因为模板函数是在编译时确定的,而虚拟多态是在运行时确定的:它们不能一起使用。
#if defined(__linux__)
#pragma pack(push)
#pragma pack(1)
// Your struct declaration here.
#pragma pack(pop)
#endif
#if defined(WIN32)
#pragma warning(disable:4103)
#pragma pack(push,1)
// Your struct declaration here.
#pragma pack(pop)
#endif
接下来,我们重载运算符以写入/读取 Product
的 vector
,并在控制台上输出它。经验法则:永远不要使用 size_t
,因为其大小取决于平台(32/64 位)。
simple::mem_istream& operator >> (simple::mem_istream& istm, std::vector<Product>& vec)
{
int size=0;
istm >> size;
if(size<=0)
return istm;
for(int i=0; i<size; ++i)
{
Product product;
istm >> product;
vec.push_back(product);
}
return istm;
}
simple::file_istream& operator >> (simple::file_istream& istm, std::vector<Product>& vec)
{
int size=0;
istm >> size;
if(size<=0)
return istm;
for(int i=0; i<size; ++i)
{
Product product;
istm >> product;
vec.push_back(product);
}
return istm;
}
simple::mem_ostream& operator << (simple::mem_ostream& ostm, const std::vector<Product>& vec)
{
int size = vec.size();
ostm << size;
for(size_t i=0; i<vec.size(); ++i)
{
ostm << vec[i];
}
return ostm;
}
simple::file_ostream& operator << (simple::file_ostream& ostm, const std::vector<Product>& vec)
{
int size = vec.size();
ostm << size;
for(size_t i=0; i<vec.size(); ++i)
{
ostm << vec[i];
}
return ostm;
}
void print_product(const Product& product)
{
using namespace std;
cout << "Product:" << product.product_name << ",
Price:" << product.price << ", Qty:" << product.qty << endl;
}
void print_products(const std::vector<Product>& vec)
{
for(size_t i=0; i<vec.size() ; ++i)
print_product(vec[i]);
}
我们使用下面的代码测试 Product
的重载运算符
void TestMemCustomOperatorsOnVec()
{
std::vector<Product> vec_src;
vec_src.push_back(Product("Book", 10.0f, 50));
vec_src.push_back(Product("Phone", 25.0f, 20));
vec_src.push_back(Product("Pillow", 8.0f, 10));
simple::mem_ostream out;
out << vec_src;
simple::mem_istream in(out.get_internal_vec());
std::vector<Product> vec_dest;
in >> vec_dest;
print_products(vec_dest);
}
void TestFileCustomOperatorsOnVec()
{
std::vector<Product> vec_src;
vec_src.push_back(Product("Book", 10.0f, 50));
vec_src.push_back(Product("Phone", 25.0f, 20));
vec_src.push_back(Product("Pillow", 8.0f, 10));
simple::file_ostream out("file.bin", std::ios_base::out | std::ios_base::binary);
out << vec_src;
out.flush();
out.close();
simple::file_istream in("file.bin", std::ios_base::in | std::ios_base::binary);
std::vector<Product> vec_dest;
in >> vec_dest;
print_products(vec_dest);
}
输出如下
Product:Book, Price:10, Qty:50
Product:Phone, Price:25, Qty:20
Product:Pillow, Price:8, Qty:10
源代码
所有源代码都在头文件中,只需包含 MiniBinStream.h 即可使用 stream
类。该类未使用任何 C++11/14 功能。它已在 VS2008、GCC4.4 和 Clang 3.2 上进行了测试。该类只是 fstream
的一个薄包装:我不需要解释任何内容。
// The MIT License (MIT)
// Simplistic Binary Streams 0.9
// Copyright (C) 2014, by Wong Shao Voon (shaovoon@yahoo.com)
//
// https://open-source.org.cn/licenses/MIT
//
#ifndef MiniBinStream_H
#define MiniBinStream_H
#include <fstream>
#include <vector>
#include <string>
#include <cstring>
#include <stdexcept>
#include <iostream>
namespace simple
{
class file_istream
{
public:
file_istream() {}
file_istream(const char * file, std::ios_base::openmode mode)
{
open(file, mode);
}
void open(const char * file, std::ios_base::openmode mode)
{
m_istm.open(file, mode);
}
void close()
{
m_istm.close();
}
bool is_open()
{
return m_istm.is_open();
}
bool eof() const
{
return m_istm.eof();
}
std::ifstream::pos_type tellg()
{
return m_istm.tellg();
}
void seekg (std::streampos pos)
{
m_istm.seekg(pos);
}
void seekg (std::streamoff offset, std::ios_base::seekdir way)
{
m_istm.seekg(offset, way);
}
template<typename T>
void read(T& t)
{
if(m_istm.read(reinterpret_cast<char*>(&t), sizeof(T)).bad())
{
throw std::runtime_error("Read Error!");
}
}
void read(char* p, size_t size)
{
if(m_istm.read(p, size).bad())
{
throw std::runtime_error("Read Error!");
}
}
private:
std::ifstream m_istm;
};
template<>
void file_istream::read(std::vector<char>& vec)
{
if(m_istm.read(reinterpret_cast<char*>(&vec[0]), vec.size()).bad())
{
throw std::runtime_error("Read Error!");
}
}
template<typename T>
file_istream& operator >> (file_istream& istm, T& val)
{
istm.read(val);
return istm;
}
template<>
file_istream& operator >> (file_istream& istm, std::string& val)
{
int size = 0;
istm.read(size);
if(size<=0)
return istm;
std::vector<char> vec((size_t)size);
istm.read(vec);
val.assign(&vec[0], (size_t)size);
return istm;
}
class mem_istream
{
public:
mem_istream() : m_index(0) {}
mem_istream(const char * mem, size_t size)
{
open(mem, size);
}
mem_istream(const std::vector<char>& vec)
{
m_index = 0;
m_vec.clear();
m_vec.reserve(vec.size());
m_vec.assign(vec.begin(), vec.end());
}
void open(const char * mem, size_t size)
{
m_index = 0;
m_vec.clear();
m_vec.reserve(size);
m_vec.assign(mem, mem + size);
}
void close()
{
m_vec.clear();
}
bool eof() const
{
return m_index >= m_vec.size();
}
std::ifstream::pos_type tellg()
{
return m_index;
}
bool seekg (size_t pos)
{
if(pos<m_vec.size())
m_index = pos;
else
return false;
return true;
}
bool seekg (std::streamoff offset, std::ios_base::seekdir way)
{
if(way==std::ios_base::beg && offset < m_vec.size())
m_index = offset;
else if(way==std::ios_base::cur && (m_index + offset) < m_vec.size())
m_index += offset;
else if(way==std::ios_base::end && (m_vec.size() + offset) < m_vec.size())
m_index = m_vec.size() + offset;
else
return false;
return true;
}
const std::vector<char>& get_internal_vec()
{
return m_vec;
}
template<typename T>
void read(T& t)
{
if(eof())
throw std::runtime_error("Premature end of array!");
if((m_index + sizeof(T)) > m_vec.size())
throw std::runtime_error("Premature end of array!");
std::memcpy(reinterpret_cast<void*>(&t), &m_vec[m_index], sizeof(T));
m_index += sizeof(T);
}
void read(char* p, size_t size)
{
if(eof())
throw std::runtime_error("Premature end of array!");
if((m_index + size) > m_vec.size())
throw std::runtime_error("Premature end of array!");
std::memcpy(reinterpret_cast<void*>(p), &m_vec[m_index], size);
m_index += size;
}
void read(std::string& str, const unsigned int size)
{
if (eof())
throw std::runtime_error("Premature end of array!");
if ((m_index + str.size()) > m_vec.size())
throw std::runtime_error("Premature end of array!");
str.assign(&m_vec[m_index], size);
m_index += str.size();
}
private:
std::vector<char> m_vec;
size_t m_index;
};
template<>
void mem_istream::read(std::vector<char>& vec)
{
if(eof())
throw std::runtime_error("Premature end of array!");
if((m_index + vec.size()) > m_vec.size())
throw std::runtime_error("Premature end of array!");
std::memcpy(reinterpret_cast<void*>(&vec[0]), &m_vec[m_index], vec.size());
m_index += vec.size();
}
template<typename T>
mem_istream& operator >> (mem_istream& istm, T& val)
{
istm.read(val);
return istm;
}
template<>
mem_istream& operator >> (mem_istream& istm, std::string& val)
{
int size = 0;
istm.read(size);
if(size<=0)
return istm;
istm.read(val, size);
return istm;
}
class file_ostream
{
public:
file_ostream() {}
file_ostream(const char * file, std::ios_base::openmode mode)
{
open(file, mode);
}
void open(const char * file, std::ios_base::openmode mode)
{
m_ostm.open(file, mode);
}
void flush()
{
m_ostm.flush();
}
void close()
{
m_ostm.close();
}
bool is_open()
{
return m_ostm.is_open();
}
template<typename T>
void write(const T& t)
{
m_ostm.write(reinterpret_cast<const char*>(&t), sizeof(T));
}
void write(const char* p, size_t size)
{
m_ostm.write(p, size);
}
private:
std::ofstream m_ostm;
};
template<>
void file_ostream::write(const std::vector<char>& vec)
{
m_ostm.write(reinterpret_cast<const char*>(&vec[0]), vec.size());
}
template<typename T>
file_ostream& operator << (file_ostream& ostm, const T& val)
{
ostm.write(val);
return ostm;
}
template<>
file_ostream& operator << (file_ostream& ostm, const std::string& val)
{
int size = val.size();
ostm.write(size);
if(val.size()<=0)
return ostm;
ostm.write(val.c_str(), val.size());
return ostm;
}
file_ostream& operator << (file_ostream& ostm, const char* val)
{
int size = std::strlen(val);
ostm.write(size);
if(size<=0)
return ostm;
ostm.write(val, size);
return ostm;
}
class mem_ostream
{
public:
mem_ostream() {}
void close()
{
m_vec.clear();
}
const std::vector<char>& get_internal_vec()
{
return m_vec;
}
template<typename T>
void write(const T& t)
{
std::vector<char> vec(sizeof(T));
std::memcpy(reinterpret_cast<void*>(&vec[0]), reinterpret_cast<const void*>(&t), sizeof(T));
write(vec);
}
void write(const char* p, size_t size)
{
for(size_t i=0; i<size; ++i)
m_vec.push_back(p[i]);
}
private:
std::vector<char> m_vec;
};
template<>
void mem_ostream::write(const std::vector<char>& vec)
{
m_vec.insert(m_vec.end(), vec.begin(), vec.end());
}
template<typename T>
mem_ostream& operator << (mem_ostream& ostm, const T& val)
{
ostm.write(val);
return ostm;
}
template<>
mem_ostream& operator << (mem_ostream& ostm, const std::string& val)
{
int size = val.size();
ostm.write(size);
if(val.size()<=0)
return ostm;
ostm.write(val.c_str(), val.size());
return ostm;
}
mem_ostream& operator << (mem_ostream& ostm, const char* val)
{
int size = std::strlen(val);
ostm.write(size);
if(size<=0)
return ostm;
ostm.write(val, size);
return ostm;
}
} // ns simple
#endif // MiniBinStream_H
0.9.5 版本的重大更改
现在需要 C++11。这些类是模板。
template<typename same_endian_type>
class file_istream {...}
template<typename same_endian_type>
class mem_istream {...}
template<typename same_endian_type>
class ptr_istream {...}
template<typename same_endian_type>
class file_ostream {...}
template<typename same_endian_type>
class mem_ostream {...}
如何将 same_endian_type
传递给类?使用 std::is_same<>()
。
// 1st parameter is data endian and 2 parameter is platform endian, if they are different, swap.
using same_endian_type = std::is_same<simple::BigEndian, simple::LittleEndian>;
simple::mem_ostream<same_endian_type> out;
out << (int64_t)23 << (int64_t)24 << "Hello world!";
simple::ptr_istream<same_endian_type> in(out.get_internal_vec());
int64_t num1 = 0, num2 = 0;
std::string str;
in >> num1 >> num2 >> str;
cout << num1 << "," << num2 << "," << str << endl;
如果您的数据和平台始终共享相同的字节序,则可以通过直接指定 std::true_type
来跳过测试。
simple::mem_ostream<std::true_type> out;
out << (int64_t)23 << (int64_t)24 << "Hello world!";
simple::ptr_istream<std::true_type> in(out.get_internal_vec());
int64_t num1 = 0, num2 = 0;
std::string str;
in >> num1 >> num2 >> str;
cout << num1 << "," << num2 << "," << str << endl;
编译时检查的优势
- 对于
same_endian_type = true_type
,交换函数是一个空函数,已被优化掉。 - 对于
same_endian_type = false_type
,交换是在没有任何先前的运行时检查成本的情况下完成的。
编译时检查的缺点
- 无法解析有时具有不同字节序的文件/数据。我相信这种情况很少见。
交换函数如下所示
enum class Endian
{
Big,
Little
};
using BigEndian = std::integral_constant<Endian, Endian::Big>;
using LittleEndian = std::integral_constant<Endian, Endian::Little>;
template<typename T>
void swap(T& val, std::true_type)
{
// same endian so do nothing.
}
template<typename T>
void swap(T& val, std::false_type)
{
std::is_integral<T> is_integral_type;
swap_if_integral(val, is_integral_type);
}
template<typename T>
void swap_if_integral(T& val, std::false_type)
{
// T is not integral so do nothing
}
template<typename T>
void swap_if_integral(T& val, std::true_type)
{
swap_endian<T, sizeof(T)>()(val);
}
template<typename T, size_t N>
struct swap_endian
{
void operator()(T& ui)
{
}
};
template<typename T>
struct swap_endian<T, 8>
{
void operator()(T& ui)
{
union EightBytes
{
T ui;
uint8_t arr[8];
};
EightBytes fb;
fb.ui = ui;
// swap the endian
std::swap(fb.arr[0], fb.arr[7]);
std::swap(fb.arr[1], fb.arr[6]);
std::swap(fb.arr[2], fb.arr[5]);
std::swap(fb.arr[3], fb.arr[4]);
ui = fb.ui;
}
};
template<typename T>
struct swap_endian<T, 4>
{
void operator()(T& ui)
{
union FourBytes
{
T ui;
uint8_t arr[4];
};
FourBytes fb;
fb.ui = ui;
// swap the endian
std::swap(fb.arr[0], fb.arr[3]);
std::swap(fb.arr[1], fb.arr[2]);
ui = fb.ui;
}
};
template<typename T>
struct swap_endian<T, 2>
{
void operator()(T& ui)
{
union TwoBytes
{
T ui;
uint8_t arr[2];
};
TwoBytes fb;
fb.ui = ui;
// swap the endian
std::swap(fb.arr[0], fb.arr[1]);
ui = fb.ui;
}
};
代码托管在 Github。
- 2016-08-01:版本 0.9.4 更新:添加了
ptr_istream
,它与mem_istream
共享相同的接口,只是它不复制数组 - 2016-08-06:版本 0.9.5 更新:添加了字节序交换
- 2017-02-16:版本 0.9.6 使用 C 文件 API,而不是 STL 文件流
- 2017-02-16:版本 0.9.7 添加了
memfile_istream
0.9.7(C 文件 API)与 0.9.5(C++ 文件流)的基准测试
# File streams (C++ File stream versus C file API) old::file_ostream: 359ms old::file_istream: 416ms new::file_ostream: 216ms new::file_istream: 328ms new::memfile_ostream: 552ms new::memfile_istream: 12ms # In-memory streams (No change in source code) new::mem_ostream: 534ms new::mem_istream: 16ms new::ptr_istream: 15ms
- 2017-03-07:版本 0.9.8:修复了 GCC 和 Clang 模板错误
- 2017-08-17:版本 0.9.9:修复了读取空
string
时获取先前值的错误 - 2018-01-23:版本 1.0.0:修复了读取
string
时的缓冲区溢出错误(由 imtrobin 报告) - 2018-05-14:版本 1.0.1:修复了 macxfadz 报告的
memfile_istream tellg
和seekg
错误,并使用is_arithmetic
代替is_integral
来确定类型是整数还是可以交换的浮点数 - 2018-08-12:版本 1.0.2:添加了重载的文件打开函数,该函数接受宽字符字符串中的文件参数。(仅在 win32 上可用)
相关文章
- C++: 简约 CSV 流
- C++14: 基于 C 文件 API 的 CSV 流
- C++: 简化二进制流
- C++: 新的文本流