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

Using std:vector as low level buffer

本文关键字:缓冲区 std vector 使用      更新时间:2023-10-16

这里的用法与直接在 C++ std:vector 中使用 read(( 相同,但需要重新分配。

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

#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);
        }
    }
}

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

read()中使用&buf[bufLen]肯定是一种未定义的行为,但&buf[0] + bufLen得到了正确的写入位置,因为连续分配是有保证的,不是吗?(initSize != 0提供。请注意,std::vector<char> buf(initSize);大小bufinitSize 。是的,如果initSize == 0,我的环境中会出现一个rumtime致命错误。我错过了什么吗?这也是一个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

在这里,每buf.size() <= i < buf.capacity()都可以替换 200,并且类似的扣除额成立。

更新 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++作为我的工作语言,现在我是一名博士生!此外,尽管许多人认为有UB,但他们给出的原因却大不相同(有些已经被证明不是UB(,这表明这是一个复杂的案例。因此,在投票和写答案之前,强烈建议阅读并参与评论

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

reserve实际上并没有向向量添加空间,它只是确保在调整其大小时不需要重新分配。 与其使用reserve不如使用resize,然后在知道实际读取多少字节后进行最后的resize

reserve保证所做的只是防止迭代器和指针失效,因为您将向量的大小增加到 capacity() .保证维护这些保留字节的内容,除非它们是size()的一部分。

例如,使用 Debug 标志生成的代码通常会包含额外的功能,以便更轻松地查找 bug。 也许新分配的内存将被一个定义良好的模式填充。 也许该类会定期扫描该内存以查看它是否已更改,如果假设只有错误可能导致该更改,则会引发异常。 这样的实现仍将符合标准。

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

如果你真的想要一个未初始化字节的缓冲区,std::vector<char>可能是错误的工具。 您应该改为查看智能指针,例如std::unique_ptr<char>

答案中的粗体文本是我的主要主张。我通过引用/参考标准付出了应有的努力和谨慎,但我愿意接受我的阅读/理解可能存在差距/错误的可能性。

我读C++03标准是因为它更短,更容易,我相信相关部分在最新标准中本质上是相同的。简而言之,问题的最后两个代码块中没有UB,因为reserve() ed内存是行为良好的对象,并且vector操作对对象的影响由标准定义。

在问题的更新 1 中表明,连续内存由 reserve() 分配,无需重新分配,我们可以在其中获取正确的地址。(如果需要,我可以提供相应的标准文本。更可疑的部分是是否可以像问题中那样访问分配的内存(基本上,我们是否可以安全地读取/写入内存(。让我们进入这个。

首先,内存不在某个"暂存空间"中。 reserve()使用vectorallocator来分配内存。allocator使用运算符new(标准 20.4.1.1(,后者又调用分配函数 (18.4.1.1(。因此,存储持续时间直到在内存(3.7.3(上调用释放(例如,delete(。会有关于寿命的担忧,但这实际上对我们来说没有问题(见下文(。

其次,它真的像马克所说的"还没有对它们做任何事情——那里没有建造任何物体"吗?首先,什么是对象?(1.8("对象是一个存储区域","具有影响其生存期(3.8(的存储持续时间(3.7("以及类型(3.9(。对我们来说重要的是,"一个对象是由[...]一个新的表达创造的"。因此,我们应该说一个对象(这里类型为 char(是使用 allocator 创建的,而不是"什么都不做"!(当然,对象没有初始化,但这对我们来说没有问题。对我们来说也很重要,因为charPOD,所以分配对象的生存期在获得存储后立即开始 (3.8 1(。对于任何 POD 对象,我们可以从中memcpy和返回,并且存储在那里的值保持不变,即使该值对于该类型无效(例如,未初始化的垃圾(!(3.9 2).因此,我们有权读/写内存(作为char对象(。此外,我们可以使用该类型的其他已定义操作(例如"="(,因为对象在生命周期中。

通常,我们可以按照问题的最后一部分的建议使用像缓冲区这样的 POD 向量。特别是,从size()访问 POD 向量的reserve() ed 内存是明确定义的。准确地说,我们可以访问 &vec[m] + n 指向的内存 ,其中m < size()m+n < capacity() (但&vec[m+n]是 UB!

请记住,我们仍然有 旧size() ,我们甚至可以推理vector方法的定义行为。例如,reserve()触发重新分配后,不会复制size()内存。由于reserve()仅分配(或重新分配((未初始化(内存,因此容器只需要将size()中的对象复制到重新分配的内存中,并且在size()内存之外应保持未初始化状态。

PS:最后一个示例来自TC++PL 4ed,应该仅适用于C++11及更高版本。在 C++11 及更高版本中,string 的内存是连续的,但对于较低版本则不是("&s[0]"是否指向 std::string 中的连续字符?(。

编辑:马克在评论中提出了一个很好的观点:即使我们可以访问reserve() ed内存,它是否会被我们无法控制的vector写入?我相信不会。容器上的每个操作(方法,algorithm(都有一个标准定义的效果,通过专门的"效果"段落或总体要求(23.1(。因此,如果操作对reserve()内存有影响,则标准指定它

例如,erase(p1,p2)的效果是"擦除范围 [q1, q2( 中的元素"(23.1.1(和"在擦除点或之后使迭代器和引用无效"(23.2.4.4(。因此,erase()reserve()记忆没有影响。

另一方面,我们知道insert()reserve()记忆有影响,但这是可以推理的,从这个意义上说,我们可以控制。标准中没有任何地方说任何容器操作都具有"可以定期清除 [ size() ] 以外的任何东西"的效果,所以它不应该这样做!