使用std:Vector作为低级缓冲区

人气:802 发布:2022-10-16 标签: c++ stl undefined-behavior language-lawyer stdvector

问题描述

此处的用法与Using read() directly into a C++ std:vector相同,但有重新分配的帐户。

输入文件的大小未知,因此当文件大小超过缓冲区大小时,会通过加倍大小来重新分配缓冲区。以下是我的代码:

#include <vector>
#include <fstream>
#include <iostream>

int main()
{
    const size_t initSize = 1;
    std::vector<char> buf(initSize); // sizes buf to initSize, so &buf[0] below is valid
    std::ifstream ifile("D:\Pictures\input.jpg", std::ios_base::in|std::ios_base::binary);
    if (ifile)
    {
        size_t bufLen = 0;
        for (buf.reserve(1024); !ifile.eof(); buf.reserve(buf.capacity() << 1))
        {
            std::cout << buf.capacity() << std::endl;
            ifile.read(&buf[0] + bufLen, buf.capacity() - bufLen);
            bufLen += ifile.gcount();
        }
        std::ofstream ofile("rebuild.jpg", std::ios_base::out|std::ios_base::binary);
        if (ofile)
        {
            ofile.write(&buf[0], bufLen);
        }
    }
}

程序按预期打印矢量容量,并写入与输入大小完全相同的输出文件,但在偏移前只有与输入相同的字节initSize,在偏移后全部为零.

read()中使用&buf[bufLen]肯定是一种未定义的行为,但是&buf[0] + bufLen获得了正确的写入位置,因为保证了连续分配,不是吗?(提供initSize != 0。请注意,std::vector<char> buf(initSize);大小为bufinitSize。是的,如果initSize == 0,我的环境中出现了传言致命错误。)我错过什么了吗?这也是UB吗?该标准是否说明了std::Vector的这种用法?

是的,我知道我们可以先计算文件大小并分配完全相同的缓冲区大小,但是在我的项目中,可以预期输入文件几乎总是小于某个SIZE,所以我可以将initSize设置为SIZE,并且不会产生任何开销(比如计算文件大小),并且只将重新分配用于异常处理和重新分配。是的,我知道我可以用resize()替换reserve(),用size()替换capacity(),然后只需很少的开销(每次调整大小时将缓冲区清零),但我仍然希望消除任何冗余操作,这只是一种偏执.

更新1:

实际上我们可以从&buf[0] + bufLen定位的标准中逻辑推导出,考虑一下:

std::vector<char> buf(128);
buf.reserve(512);
char* bufPtr0 = &buf[0], *bufPtrOutofRange = &buf[0] + 200;
buf.resize(256); std::cout << "standard guarantees no reallocation" << std::endl;
char* bufPtr1 = &buf[0], *bufInRange = &buf[200]; 
if (bufPtr0 == bufPtr1)
    std::cout << "so bufPtr0 == bufPtr1" << std::endl;
std::cout << "and 200 < buf.size(), standard guarantees bufInRange == bufPtr1 + 200" << std::endl;
if (bufInRange == bufPtrOutofRange)
    std::cout << "finally we have: bufInRange == bufPtrOutofRange" << std::endl;

输出:

standard guarantees no reallocation
so bufPtr0 == bufPtr1
and 200 < buf.size(), standard guarantees bufInRange == bufPtr1 + 200
finally we have: bufInRange == bufPtrOutofRange

这里的200可以用每buf.size() <= i < buf.capacity()替换一次,类似的推论也成立。

更新2:

是的,我确实错过了一些东西.但是问题不在于连续性(参见更新1),甚至不在于无法写入内存(参见我的答案)。今天我花了一些时间研究这个问题,程序获得了正确的地址,将正确的数据写入保留内存,但是在下一个reserve()中,buf被重新分配,并且只将范围[0, buf.size())中的元素复制到新内存中。这就是整个谜题的答案……

最后注意:如果缓冲区填满某些数据后不需要重新分配,您绝对可以使用reserve()/capatity()而不是resize()/size(),但如果需要,可以使用后者。另外,在这里提供的所有实现(VC++、g++、ICC)下,示例都会按预期运行:

const size_t initSize = 1;
std::vector<char> buf(initSize);
buf.reserve(1024*100); // assume the reserved space is enough for file reading
std::ifstream ifile("D:\Pictures\input.jpg", std::ios_base::in|std::ios_base::binary);
if (ifile)
{
    ifile.read(&buf[0], buf.capacity());  // ok. the whole file is read into buf
    std::ofstream ofile("rebuld.jpg", std::ios_base::out|std::ios_base::binary);
    if (ofile)
    {
        ofile.write(&buf[0], ifile.gcount()); // rebuld.jpg just identical to input.jpg
    }
}
buf.reserve(1024*200); // horror! probably always lose all data in buf after offset initSize

这里是另一个示例,引用自‘TC++PL,4E’pp 1041,请注意,函数中的第一行使用reserve()而不是resize()

void fill(istream& in, string& s, int max)
// use s as target for low-level input (simplified)
{
    s.reserve(max); // make sure there is enough allocated space
    in.read(&s[0],max);
    const int n = in.gcount(); // number of characters read
    s.resize(n);
    s.shrink_to_fit();  // discard excess capacity
}

更新3(时隔8年):这几年发生了很多事情,我已经将近6年没有用C++作为我的工作语言了,现在我是一名博士生了!此外,尽管许多人认为存在瑞银,但他们给出的原因却截然不同(一些人已经被证明不是瑞银),这表明这是一个复杂的案例。因此,在投票和写答案之前,强烈建议您阅读并参与评论。

另一件事是,通过博士培训,我现在可以相对轻松地研究C++标准,这在几年前我还不敢。我相信我在我自己的回答中已经说明,根据标准,上述两个代码块应该起作用。(string示例需要C++11。)由于我的回答仍然存在争议(但我相信不是伪造的),我不接受它,而是对批评性评论和其他答案持开放态度。

推荐答案

reserve实际上不会将空间添加到向量,它只是确保在调整向量大小时不需要重新分配。您应该使用resize,而不是使用reserve,然后在知道实际读取的字节数后执行最后一次resize

当您将向量大小增加到capacity()时,reserve可以保证做的所有事情都是防止迭代器和指针失效。不保证维护这些保留字节的内容,除非它们是size()的一部分。

例如,使用Debug标志构建的代码通常包含额外的功能,以便更容易找到bug。也许新分配的内存将被定义良好的模式填满。也许这个类会定期扫描该内存以查看它是否已更改,如果已更改,则在假设只有bug可能导致更改的情况下抛出异常。这样的实现仍然是符合标准的。

std::string的示例甚至更好,因为有一个案例几乎肯定会失败。string::c_str()将返回一个指向末尾带有空终止符的字符串的指针。现在,一致性实现可以为终止NULL分配第二个缓冲区,并在复制字符串后返回该指针,但这将非常浪费。更有可能的是,String类将只确保其保留的缓冲区有空间容纳额外的空字符,并根据需要在其中写入一个空字符。但是标准并没有规定何时写入该NULL,它可以在对c_str的调用中,也可以在字符串可能被修改的任何位置。因此您无法知道您的其中一个字节何时会被覆盖。

如果您确实需要未初始化字节的缓冲区,std::vector<char>可能无论如何都是错误的工具。您应该改为使用智能指针,如std::unique_ptr<char>

236