二进制版本的iostream

时间:2009-07-19 20:41:42

标签: c++ binary iostream streambuf

我一直在编写iostreams的二进制版本。它本质上允许您编写二进制文件,但可以很好地控制文件的格式。用法示例:

my_file << binary::u32le << my_int << binary::u16le << my_string;

将my_int写为无符号的32位整数,将my_string写为长度前缀的字符串(前缀为u16le。)要读回文件,可以翻转箭头。效果很好。然而,我在设计上遇到了一个障碍,我仍然围着它。所以,是时候问问了。 (我们做了几个假设,例如8位字节,2s补码整数和IEEE浮点数。)

引擎盖下的iostream使用streambufs。这真是一个很棒的设计 - iostreams将“int”的序列化编码到文本中,让底层的streambuf处理剩下的部分。因此,你得到cout,fstreams,stringstreams等。所有这些,包括iostream和streambufs,都是模板化的,通常是在char上,但有时也是一个wchar。但是,我的数据是一个字节流,最好用'unsigned char'表示。

我的第一次尝试是基于unsigned char对类进行模板化。 std::basic_string模板足够好,但streambuf没有。我遇到了一个名为codecvt的类的问题,我永远无法遵循unsigned char主题。这提出了两个问题:

1)为什么streambuf对此类事情负责?似乎代码转换不属于streambuf的职责 - streambufs应该采用流,并将数据缓冲到/来自它。而已。像代码转换一样高级的东西感觉它应该属于iostreams。

由于我无法使用模板化的streambuf来处理unsigned char,我回到char,只是在char / unsigned char之间传递数据。出于显而易见的原因,我试图尽量减少演员阵容。大多数数据基本上都是在read()或write()函数中结束,然后调用底层的streambuf。 (并在此过程中使用强制转换。)read函数基本上是:

size_t read(unsigned char *buffer, size_t size)
{
    size_t ret;
    ret = stream()->sgetn(reinterpret_cast<char *>(buffer), size);
    // deal with ret for return size, eof, errors, etc.
    ...
}

好的解决方案,糟糕的解决方案?


前两个问题表明需要更多信息。首先,查看了boost :: serialization等项目,但它们存在于更高级别,因为它们定义了自己的二进制格式。这更适用于较低级别的读/写,希望定义格式,或者已经定义了格式,或者不需要或不需要批量元数据。

其次,有些人询问了binary::u32le修饰符。它是一个类的实例化,它具有所需的字节顺序和宽度,此刻可能是未来的签名。该流保存该类的最后传递的实例的副本,并在序列化中使用该副本。这是一个解决方法,我试图重载&lt;&lt;操作员因此:

bostream &operator << (uint8_t n);
bostream &operator << (uint16_t n);
bostream &operator << (uint32_t n);
bostream &operator << (uint64_t n);

但当时,这似乎不起作用。我有一些模糊函数调用的问题。对于常数来说尤其如此,尽管你可以像一张海报所说的那样,施放或仅仅将其声明为const <type>。我似乎记得还有一些其他更大的问题。

5 个答案:

答案 0 :(得分:2)

我同意合法化。我需要做几乎正是你正在做的事情,看着重载<< / >>,但得出的结论是iostream并不是为了容纳它而设计的。首先,我不想将流类子类化以便能够定义我的重载。

我的解决方案(只需要在一台机器上临时序列化数据,因此不需要解决字节顺序)是基于这种模式:

// deducible template argument read
template <class T>
void read_raw(std::istream& stream, T& value,
    typename boost::enable_if< boost::is_pod<T> >::type* dummy = 0)
{
    stream.read(reinterpret_cast<char*>(&value), sizeof(value));
}

// explicit template argument read
template <class T>
T read_raw(std::istream& stream)
{
    T value;
    read_raw(stream, value);
    return value;
}

template <class T>
void write_raw(std::ostream& stream, const T& value,
    typename boost::enable_if< boost::is_pod<T> >::type* dummy = 0)
{
    stream.write(reinterpret_cast<const char*>(&value), sizeof(value));
}

然后我为任何非POD类型(例如字符串)进一步重载了read_raw / write_raw。请注意,只需要重载read_raw的第一个版本;如果你use ADL correctly,第二个(1-arg)版本可以调用稍后定义的2-arg重载和其他命名空间。

写例:

int32_t x;
int64_t y;
int8_t z;
write_raw(is, x);
write_raw(is, y);
write_raw<int16_t>(is, z); // explicitly write int8_t as int16_t

阅读示例:

int32_t x = read_raw<int32_t>(is); // explicit form
int64_t y;
read_raw(is, y); // implicit form
int8_t z = numeric_cast<int8_t>(read_raw<int16_t>(is));

它不像重载运算符那样性感,并且事情不容易在一行上(我倾向于避免,因为调试断点是面向行的),但我认为它变得更简单,更明显,而且不那么冗长。

答案 1 :(得分:1)

据我了解,您用于指定类型的流属性更适合指定字节序,打包或其他“元数据”值。类型本身的处理应该由编译器完成。至少,这就是STL的设计方式。

如果使用重载来自动分离类型,则只有在与声明的变量类型不同时才需要指定类型:

Stream& operator<<(int8_t);
Stream& operator<<(uint8_t);
Stream& operator<<(int16_t);
Stream& operator<<(uint16_t);
etc.

uint32_t x;
stream << x << (uint16_t)x;

读取除声明类型以外的类型会有点麻烦。但是,一般来说,我应该避免读取或写入与输出类型不同的类型的变量。

我相信std :: codecvt的默认版本什么都不做,为所有内容返回“noconv”。它只在使用“宽”字符流时才真正起作用。你不能为codecvt设置一个类似的定义吗?如果由于某种原因,为您的流定义无操作编解码器是不切实际的,那么我认为您的投射解决方案没有任何问题,特别是因为它被隔离到一个位置。

最后,您确定使用某些标准序列化代码(例如Boost)会不会更好,而不是自己动手编写?

答案 2 :(得分:0)

我们需要做一些与你正在做的事情类似的事情,但我们遵循另一条道路。我对您如何定义界面感兴趣。我不知道你可以处理的部分原因是你定义的操纵器(binary :: u32le,binaryu16le)。

使用basic_streams,操纵器控制如何读取/写入所有后续元素,但在您的情况下,它可能没有意义,因为大小(操纵器信息的一部分)受传入的变量的影响,进行。

binary_istream in;
int i;
int i2;
short s;
in >> binary::u16le >> i >> binary::u32le >> i2 >> s;

在上面的代码中,确定i变量是否为32位(假设int为32位)是有意义的,您想要从序列化流中仅提取16位,而您想要提取全部32位进入i2。之后,要么用户被迫为传入的每个其他类型引入操纵器,要么操纵器仍然有效,并且当传入short并且读取32位时可能存在溢出,并且以任何方式用户可能会得到意想不到的结果。

尺寸似乎不属于(在我看来)操纵者。

就像我们的情况一样,在我们的情况下,因为我们有其他约束作为类型的运行时定义,我们最终构建了我们自己的元类型系统来在运行时构建类型(一种变体),然后我们最终为这些类型(boost样式)实现了de / serialization,所以我们的序列化程序不能使用基本的C ++类型,而是使用序列化/数据对。

答案 3 :(得分:0)

我不会使用operator&lt;&lt;因为它与格式化文本I / O密切相关。

实际上,我根本不会使用运算符重载。我会找到另一个成语。

答案 4 :(得分:0)

在现代c ++中,可以通过使用string_view将<<与二进制数据一起使用,因为它不是以null终止并且可以显式设置大小的。

char buf[] = "this buffer can hold binary data, including null characters";
cout << string_view(buf, sizeof(buf));