将C++向量(vector)读写到文件中

19

为了进行一些图形工作,我需要尽可能快地读入大量数据,并希望直接将数据结构读入和写入磁盘。基本上,我有许多不同文件格式的 3D 模型需要加载,这需要太长时间,因此我想将它们以其“准备好”的格式写出,作为缓存,在程序的后续运行中加载得更快。

这样做安全吗? 我的担心在于直接读取向量的数据?我已经删除了错误检查,硬编码了整数的大小等内容,以便提供一个简短的工作示例。我知道这是糟糕的代码,但我真正想问的问题是,像这样直接将整个结构体数组读入向量是否在 C++ 中是安全的?我相信是可以的,但是当你开始使用底层和直接处理原始内存时,C++ 会有很多陷阱和未定义的行为。

我意识到数字格式和大小可能会在平台和编译器之间发生改变,但这只会被同一编译器程序读取和写入,以缓存数据,以备稍后运行同一程序时可能需要。

#include <fstream>
#include <vector>

using namespace std;

struct Vertex
{
    float x, y, z;
};

typedef vector<Vertex> VertexList;

int main()
{
    // Create a list for testing
    VertexList list;
    Vertex v1 = {1.0f, 2.0f,   3.0f}; list.push_back(v1);
    Vertex v2 = {2.0f, 100.0f, 3.0f}; list.push_back(v2);
    Vertex v3 = {3.0f, 200.0f, 3.0f}; list.push_back(v3);
    Vertex v4 = {4.0f, 300.0f, 3.0f}; list.push_back(v4);

    // Write out a list to a disk file
    ofstream os ("data.dat", ios::binary);

    int size1 = list.size();
    os.write((const char*)&size1, 4);
    os.write((const char*)&list[0], size1 * sizeof(Vertex));
    os.close();


    // Read it back in
    VertexList list2;

    ifstream is("data.dat", ios::binary);
    int size2;
    is.read((char*)&size2, 4);
    list2.resize(size2);

     // Is it safe to read a whole array of structures directly into the vector?
    is.read((char*)&list2[0], size2 * sizeof(Vertex));

}

7
尽量避免使用魔法常量:os.write(&size1, sizeof(size1))比在此处硬编码4更好。同样的,读取也是如此。 - David Rodríguez - dribeas
2
@David,请尽量避免在发表评论之前不阅读问题 ;) - Peter Alexander
3
@Poita_ :) 我知道这个更改只是为了紧凑性,但事实是4只比sizeof(int)稍小一点,后者应始终优先考虑,即使在合成的代码片段中也是如此。 - David Rodríguez - dribeas
@David 确实。我真的同意,只是为了我的示例而懒惰。 - jcoder
有10k人浏览,但被接受的答案只有8个赞? - Almo
6个回答

21

正如Laurynas所说,std::vector保证是连续的,因此应该可以工作,但这可能是不可移植的。

在大多数系统上,sizeof(Vertex)将为12,但结构体被填充以使sizeof(Vertex) == 16也很常见。如果您在一个系统上编写数据,然后在另一个系统上读取该文件,则不能保证它会正确地工作。


读写填充字节只会减慢速度。我会编写一个非常简单的operator<<(ostream..)并一次读取一个浮点数(在概念上)。 - Jan
2
更不用说在这种情况下使你的文件增大了33%。 - Peter Alexander
@Jan 阅读(大概)一个文本文件并将数字转换为浮点数比直接读取二进制文件更昂贵,即使它更大。文本文件也可能更大,除非所有值都少于四位数。 - KeithB
我正在合并多个3D模型以制作一个模型,这不仅涉及读取多个复杂格式的磁盘文件,还需要进行相当多的数学计算来转换、旋转和缩放坐标,但这只需要做一次,因此缓存是一个很大的优势...虽然我现在已经修改了我的代码,直接读入到Direct3D顶点缓冲区中,所以答案不再相关,但对我仍然很有趣 :) - jcoder
1
@KeithB:我的意思是建议像JB最初那样实现流操作符: ostream& operator<<(ostream& str, Vertex v) { str.write((const char*)&v.x, sizeof(v.x)); ... 在一个充满顶点的文件中,你只需要按照正确的顺序读取它们,就像以前一样,但这有助于阅读,并处理填充问题。 - Jan

11
你可能会对Boost.Serialization库感兴趣。它知道如何将STL容器保存/加载到磁盘等操作。对于你的简单示例来说,它可能有点过头了,但如果你在程序中进行其他类型的序列化,则可能会更有用。
这是一些实现你所需功能的示例代码:
#include <algorithm>
#include <fstream>
#include <vector>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/serialization/vector.hpp>

using namespace std;

struct Vertex
{
    float x, y, z;
};

bool operator==(const Vertex& lhs, const Vertex& rhs)
{
    return lhs.x==rhs.x && lhs.y==rhs.y && lhs.z==rhs.z;
}

namespace boost { namespace serialization {
    template<class Archive>
    void serialize(Archive & ar, Vertex& v, const unsigned int version)
    {
        ar & v.x; ar & v.y; ar & v.z;
    }
} }

typedef vector<Vertex> VertexList;

int main()
{
    // Create a list for testing
    const Vertex v[] = {
        {1.0f, 2.0f,   3.0f},
        {2.0f, 100.0f, 3.0f},
        {3.0f, 200.0f, 3.0f},
        {4.0f, 300.0f, 3.0f}
    };
    VertexList list(v, v + (sizeof(v) / sizeof(v[0])));

    // Write out a list to a disk file
    {
        ofstream os("data.dat", ios::binary);
        boost::archive::binary_oarchive oar(os);
        oar << list;
    }

    // Read it back in
    VertexList list2;

    {
        ifstream is("data.dat", ios::binary);
        boost::archive::binary_iarchive iar(is);
        iar >> list2;
    }

    // Check if vertex lists are equal
    assert(list == list2);

    return 0;
}
请注意,我必须在boost::serialization命名空间中实现一个serialize函数来对你的Vertex进行序列化。这样可以让序列化库知道如何序列化Vertex成员。
我浏览了一下boost::binary_oarchive源代码,它似乎直接从/向流缓冲区读取/写入原始向量数组数据。因此速度应该很快。

谢谢。也许对我所需的有些过度,但我一定会研究一下。 - jcoder

8

std::vector保证在内存中是连续的,所以可以肯定地回答是。


3
我遇到了完全相同的问题。
首先,这些语句有问题。
os.write((const char*)&list[0], size1 * sizeof(Vertex));
is.read((char*)&list2[0], size2 * sizeof(Vertex));

Vector数据结构中还有其他内容,因此这将使您的新向量填充垃圾。

解决方案:
当您将您的向量写入文件时,不用担心您的顶点类的大小,只需直接将整个向量写入内存即可。

os.write((const char*)&list, sizeof(list));

然后您可以一次性将整个向量读入内存

is.seekg(0,ifstream::end);
long size2 = is.tellg();
is.seekg(0,ifstream::beg);
list2.resize(size2);
is.read((char*)&list2, size2);

3
另一种显式读写 vector<> 文件的替代方法是将底层分配器替换为从内存映射文件中分配内存的分配器。这将允许您避免中间的读/写相关复制。但是,这种方法确实有一些开销。除非您的文件非常大,否则对于您的特定情况可能没有意义。像往常一样进行分析以确定此方法是否适合您。此方法也有一些注意事项,Boost.Interprocess 库可以很好地处理这些问题。您可能特别感兴趣的是其 分配器和容器

2
如果这个用于同一段代码的缓存,我认为没有任何问题。我在多个基于Unix的系统上使用了同样的技术而没有出现问题。作为额外的预防措施,您可能需要在文件开头编写一个具有已知值的结构,并检查它是否可以正确读取。您还可以记录文件中结构的大小。如果填充发生变化,这将节省很多调试时间。

是的,我会在文件上写一个头部以确保它只读取我期望的内容。 - jcoder

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