c++ - 使用标准 :vector as low level buffer

标签 c++ stl language-lawyer stdvector undefined-behavior

这里的用法和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);
        }
    }
}

程序按预期打印 vector 容量,并写入与输入大小相同的输出文件,但在偏移量 initSize 之前仅使用与输入相同的字节。 , 然后全为零...

使用 &buf[bufLen]read()绝对是未定义的行为,但是 &buf[0] + bufLen得到正确的位置来写,因为连续分配是有保证的,不是吗? (提供 initSize != 0 。注意 std::vector<char> buf(initSize); 大小 bufinitSize 。是的,如果 initSize == 0 ,我的环境中会发生 rumtime fatal error 。)我错过了什么吗? 这也是UB吗?标准是否说明了 std::vector 的这种用法?

是的,我知道我们可以先计算文件大小并分配完全相同的缓冲区大小,但在我的项目中,可以预期输入文件几乎总是小于某个SIZE。 , 所以我可以设置 initSizeSIZE并期望没有开销(如文件大小计算),并且仅将重新分配用于“异常处理”。是的,我知道我可以替换 reserve()resize()capacity()size() ,然后让事情以很少的开销工作(在每次调整大小时将缓冲区归零),但我仍然想摆脱任何冗余操作,只是一种偏执......

更新 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
}

Update 3(8年后):这些年发生了很多事,我有将近6年没有使用C++作为我的工作语言,现在我是一名博士生!此外,尽管许多人认为存在 UB,但他们给出的理由却大相径庭(有些已经证明不是 UB),表明这是一个复杂的案例。因此,在投票和写答案之前,强烈建议阅读并参与评论

另一件事是,通过博士培训,我现在可以相对轻松地钻研 C++ 标准,这在几年前是不敢的。 我相信我在自己的回答中表明,根据标准,上述两个代码块应该工作。(string 示例需要 C++11。)由于我的回答仍然存在争议(但我相信没有被篡改),我不接受它,而是愿意接受批评性评论和其他答案。

最佳答案

reserve实际上并没有将空间添加到 vector 中,它只是确保在调整大小时不需要重新分配。而不是使用 reserve你应该使用 resize , 然后做最后一个 resize一旦你知道你实际读入了多少字节。

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

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

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

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

关于c++ - 使用标准 :vector as low level buffer,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19064318/

相关文章:

C++ 对不同概念的不同使用声明

c++ - 为什么我的自定义迭代器不能使用 STL 拷贝?

C++ vector 在递归函数中丢失数据

c++ - 为什么在返回从函数的返回类型派生的类型的本地对象时不选择 move 构造函数?

c++ - 在 Qt C++ 中获取嵌入式资源的容器文件

c++ - MFC C++ 指向函数新手的指针

c - 成员之间不会有填充是否安全?

c++ - std::atomic<T>::notify_all 是如何排序的?

c++ - Visual Studio 中 EOF 之前的 EOF

c++ - 如何使用 boost::bind 为 std::set 定义排序标准