8

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

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

将 my_int 写为无符号 32 位整数,并将 my_string 写为长度前缀字符串(其中前缀为 u16le)。要读回文件,您可以翻转箭头。效果很好。但是,我在设计中遇到了一个问题,我仍然对此持观望态度。所以,是时候问了。(我们做了几个假设,例如 8 位字节、2s 补码整数和 IEEE 浮点数。)

iostreams,在引擎盖下,使用streambufs。这真的是一个很棒的设计——iostreams 将 ' int' 的序列化编码为文本,并让底层的 streambuf 处理其余的。因此,您会得到 cout、fstreams、stringstreams 等。所有这些,无论是 iostreams 还是 streambufs,都是模板化的,通常在 char 上,但有时也作为 wchar。但是,我的数据是字节流,最好用' unsigned char'表示。

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

1)为什么一个streambuf要对这些事情负责?似乎代码转换超出了streambuf的职责——streambufs应该接受一个流,并缓冲数据到/从它。而已。像代码转换这样高级别的东西感觉应该属于 iostreams。

由于我无法让模板化的流缓冲区与 unsigned char 一起工作,所以我回到 char,并且只是在 char/unsigned char 之间转换数据。出于显而易见的原因,我试图尽量减少演员表的数量。大多数数据基本上都在 read() 或 write() 函数中结束,然后调用底层的 streambuf。(并在过程中使用强制转换。)读取功能基本上是:

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修饰符。它是一个类的实例化,目前持有所需的字节序和宽度,将来可能是有符号的。该流包含该类的最后传递实例的副本,并在序列化中使用它。这是一种解决方法,我最初尝试过重载 << 运算符:

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

然而在当时,这似乎并没有奏效。我在模棱两可的函数调用方面遇到了几个问题。对于常量来说尤其如此,尽管您可以,正如一位海报所建议的那样,将其强制转换或仅将其声明为const <type>. 我似乎记得还有其他一些更大的问题。

4

5 回答 5

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 需要重载;如果您正确使用 ADL,第二个 (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));

它不像重载的操作符那样性感,而且事情并不容易放在一行上(无论如何我倾向于避免这种情况,因为调试断点是面向行的),但我认为它变得更简单,更明显,而且不多更详细。

于 2010-03-08T20:43:43.897 回答
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)而不是自己滚动代码不会更好吗?

于 2009-07-19T21:42:52.023 回答
0

我不会使用 operator<< 因为它与格式化文本 I/O 的关联太紧密了。

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

于 2009-07-20T02:26:15.040 回答
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. 之后,要么用户被迫为传入的每个其他类型引入操纵器,要么操纵器仍然有效,并且当传入短路并且读取 32 位时可能会溢出,并且以任何方式用户可能会得到意想不到的结果。

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

顺便说一句,在我们的例子中,因为我们有其他约束作为类型的运行时定义,我们最终构建了自己的元类型系统来在运行时构建类型(一种变体),然后我们结束了为这些类型实现反序列化(增强风格),因此我们的序列化器不适用于基本的 C++ 类型,而是使用序列化/数据对。

于 2009-07-19T21:52:12.990 回答
0

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

char buf[] = "this buffer can hold binary data, including null characters";
cout << string_view(buf, sizeof(buf));
于 2020-01-26T17:14:53.850 回答