代码之家  ›  专栏  ›  技术社区  ›  Thanatos

iostream的二进制版本

  •  7
  • Thanatos  · 技术社区  · 15 年前

    我一直在写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 ’。

    我的第一次尝试是根据 无符号字符 是的。 std::basic_string 模板足够好,但是 streambuf 没有。我遇到了一个名为 codecvt ,我永远都无法理解 无符号字符 主题。这引发了两个问题:

    1)为什么streambuf要对这些事情负责?似乎代码转换不在streambuf的职责范围之内——streambuf应该接受一个流,并将数据缓冲到该流或从该流缓冲数据。没别的了。像代码转换这样高级别的东西感觉应该属于iostreams。

    因为我无法让模板化的streambufs处理无符号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 修饰语。它是一个类的实例化,该类目前拥有所需的endianness和width,将来可能有符号ness。流保存该类的最后一个传递实例的副本,并在序列化中使用该副本。这是一种变通方法,我最初尝试重载<<运算符,然后:

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

    不过,在当时,这似乎并不奏效。我对不明确的函数调用有几个问题。这尤其适用于常量,尽管正如一张海报所建议的那样,您可以将其作为 const <type> 是的。不过,我似乎还记得还有其他更大的问题。

    4 回复  |  直到 15 年前
        1
  •  2
  •   Trevor Robinson    7 年前

    我同意合法化。我需要做的几乎和你做的完全一样,看看超载 << / >> ,但得出的结论是,iostream并不是为适应这种情况而设计的。首先,我不想为了定义重载而对流类进行子类化。

    我的解决方案(只需要在一台机器上临时序列化数据,因此不需要处理endianness)基于以下模式:

    // 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));
    

    它不像重载操作符那么性感,而且事情也不像一行那么容易(无论如何我都倾向于避免,因为调试断点是面向行的),但是我认为它变得更简单,更明显,也不太冗长。

        2
  •  1
  •   Tim Sylvester    15 年前

    据我所知,用于指定类型的流属性更适合于指定endian ness、packing或其他“元数据”值。类型本身的处理应该由编译器完成。至少,这是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设置一个类似的定义吗?如果出于某种原因,为流定义一个no-op codecvt是不切实际的,那么我认为您的转换解决方案没有任何问题,特别是因为它被隔离到一个位置。

    最后,您确定使用一些标准的序列化代码不会更好,比如 Boost 而不是自己滚?

        3
  •  0
  •   David Rodríguez - dribeas    15 年前

    我们需要做一些类似于你所做的事情,但我们遵循了另一条道路。我对你如何定义你的界面感兴趣。我不知道你怎么处理的一部分是 操纵器 您已经定义了(binary::u32le,binaryu16le)。

    对于基本的_流,操纵器控制如何读取/写入以下所有元素,但在您的情况下,这可能没有意义,因为大小(操纵器信息的一部分)受传入和传出的变量的影响。

    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位有可能溢出时,用户可能会以任何方式获得意外的结果。

    大小似乎不属于(在我看来)操纵者。

    正如我们注意到的那样,因为我们有其他约束作为类型的运行时定义,我们最终建立了自己的元类型系统来在运行时构建类型(一种变体),然后我们最终实现了那些类型(Boost样式)的去序列化,因此我们的序列化器不适用于基本的C++类型,而不是序列化/数据对。

        4
  •  0
  •   legalize    15 年前

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

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

    推荐文章