使用std::stringstream流读写二进制数据

4
我正在为一个类编写一些测试用例,该类可能会从std::istream读取并写入std::ostream。作为测试过程的一部分,我想手动创建一个测试文件数据块,将其包装在std::stringstream中,然后将其传递给我的类进行处理。
尽管它确实起作用,但我觉得我的当前解决方案缺乏优雅之处。我真的不喜欢使用那些带有reinterpret_cast的原始写入调用。
std::stringstream file;

uint32_t version = 1;
uint32_t dataSize = 10;
uint32_t recordCount = 3;

file.write(reinterpret_cast<char*>(&version), sizeof(version));
file.write(reinterpret_cast<char*>(&dataSize), sizeof(dataSize));
file.write(reinterpret_cast<char*>(&recordCount), sizeof(recordCount));

myclass.read(file)

有没有办法使用流运算符以二进制形式写入这些数据?我希望能够像下面这样做。
std::stringstream file;

uint32_t version = 1;
uint32_t dataSize = 0;
uint32_t recordCount = 3;

file << version << dataSize << recordCount;

myclass.read(file);

如果我采用这种方法,提取数字会得到一个 103 的结果,在 ascii 上下文中这是预期的,但我明显想避免以这种方式序列化我的数据。

https://dev59.com/pUjSa4cB1Zd3GeqPF39Y - oakad
1
iostream并非为此使用方式而设计。您可能需要考虑其他库,例如boost::spirit::karma或boost::serialize。 - oakad
如果您不想使用>><<格式(比如因为您不是在序列化,而是以二进制格式读取任意数据),那么定义新的流类来代替文本流也是相当容易的。这些新的流类的“契约”是某种二进制格式。@oakad - James Kanze
2个回答

3
你的代码存在问题:当你使用reinterpret_cast时,实际上你并不知道自己将要写入流中的内容,也就无法测试你的代码。如果你想测试你的代码如何处理二进制格式的字节流,你可以轻松地用任意字节流初始化一个std::istringstream
char bytes[] = { /*...*/ };
std::istringstream( std::string( std::begin( bytes ), std::end( bytes ) ) );

如果您没有C++11,您可以轻松编写自己的beginend函数。
这样,您就会确切地知道字节是什么,而不是依赖于实现如何表示任何特定类型的假设。
或者,如果您正在读写二进制数据,则可能希望定义执行此操作的类,使用>><<。这些类与std::istreamstd::ostream无关,但可以逻辑上使用std::ios_base来提供支持传统错误报告和对std::streambuf的接口。该类将具有以下成员:
namespace {

class ByteGetter
{
public:
    explicit            ByteGetter( ixdrstream& stream )
        :   mySentry( stream )
        ,   myStream( stream )
        ,   mySB( stream->rdbuf() )
        ,   myIsFirst( true )
    {
        if ( ! mySentry ) {
            mySB = NULL ;
        }
    }
    std::uint8_t        get()
    {
        int                 result = 0 ;
        if ( mySB != NULL ) {
            result = mySB->sgetc() ;
            if ( result == EOF ) {
                result = 0 ;
                myStream.setstate( myIsFirst
                    ?   std::ios::failbit | std::ios::eofbit
                    :   std::ios::failbit | std::ios::eofbit | std::ios::badbit ) ;
            }
        }
        myIsFirst = false ;
        return result ;
    }

private:
    ixdrstream::sentry  mySentry ;
    ixdrstream&         myStream ;
    std::streambuf*     mySB ;
    bool                myIsFirst ;
} ;
}

ixdrstream&
ixdrstream::operator>>( std::uint32_t&      dest )
{
    ByteGetter          source( *this ) ;
    std::uint32_t       tmp = source.get() << 24 ;
    tmp |= source.get() << 16 ;
    tmp |= source.get() <<  8 ;
    tmp |= source.get()       ;
    if ( *this ) {
        dest = tmp ;
    }
    return *this ;
}

为了最大程度的可移植性,您可能想避免使用uint8_tuint32_t类型。在此级别上,不知道类型的确切大小编写代码会更加困难,因此如果您确定永远不需要转移到可能未定义这些类型的奇特系统,则值得省去额外的工作。


我很感激这个答案。我原本希望能够在不增加复杂性的情况下满足我的需求,但只要复杂性得到隔离并且接口干净,它就符合我的总体目标。最终,我决定使用boost序列化框架而不是设计自己的接口。 - vmrob

1
您可以为输出流(不仅仅是字符串流,还包括文件流)声明运算符<<。以下代码可能有效(未经测试),但是您可能会遇到类型(uint32_t)的问题:
std::ostream& operator<<(std::ostream& stream, uint32_t value)
{
    stream.write(reinterpret_cast<char*>(&value), sizeof(value));
    return stream;
}

std::stringstream file;
file << version << dataSize << recordCount;

编辑:

由于类型值已经存在,因此<<运算符已经被定义。一种替代方法是声明一个新的运算符<=

std::ostream& operator<=(std::ostream& stream, uint32_t value);
file <= version <= dataSize <= recordCount;

这两个运算符都是从左到右进行操作的,所以这可能有效,但可能不是最好的解决方案。


正如你所想,使用那种解决方案确实会带来一些麻烦。在我的系统上,uint32_t是无符号整数的typedef,并且已经存在该类型的重载。然而,如果我想使用自定义类/类型,那么这种方法将非常有效。 - vmrob
1
我认为那会起作用。在评估了我的代码之后,我决定看一下boost::serialize库。它似乎通过重载运算符(特别是&)来实现你所建议的功能,并且还有许多其他功能,我认为这些功能对我的程序很有帮助。 - vmrob
1
这违反了<<的基本约定,不应该这样做。 - James Kanze
@0x499602D2 是的,你可以重载 <= 运算符。快速的谷歌搜索可以得到这个教程:http://www.learncpp.com/cpp-tutorial/94-overloading-the-comparison-operators/ - vmrob
1
@vmrob std::ostream<< 运算符的约定是输出将会是格式化文本。 - James Kanze
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接