65.9K
CodeProject 正在变化。 阅读更多。
Home

C++: 简化二进制流

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (21投票s)

2016年4月12日

CPOL

4分钟阅读

viewsIcon

80213

downloadIcon

1346

支持字节序交换的简化二进制流

目录

引言

不少 C++ 开发人员习惯了文本流上的 << 和 >> 运算符,但在二进制流上却错过了它们。简化的二进制流只不过是 STL fstream 的 readwrite 函数的简单包装。读者可能会将其与其他序列化库(如 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,而不是逐个处理成员。读者应该注意到,我们使用相同的代码重载内存流和文件流。这是不幸的,因为这两种类型的流不是派生自同一个基类。即使它们是,它也无法工作,因为 writeread 函数是模板函数,而模板函数不能是虚拟的,因为模板函数是在编译时确定的,而虚拟多态是在运行时确定的:它们不能一起使用。

#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

接下来,我们重载运算符以写入/读取 Productvector,并在控制台上输出它。经验法则:永远不要使用 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 tellgseekg 错误,并使用 is_arithmetic 代替 is_integral 来确定类型是整数还是可以交换的浮点数
  • 2018-08-12:版本 1.0.2:添加了重载的文件打开函数,该函数接受宽字符字符串中的文件参数。(仅在 win32 上可用)

相关文章

© . All rights reserved.