【问题标题】:Modern C++ idiom for allocating / deallocating an I/O buffer用于分配/释放 I/O 缓冲区的现代 C++ 习惯用法
【发布时间】:2015-08-16 04:18:54
【问题描述】:

对于 I/O 工作,我需要将 N 个字节读入缓冲区。 N 在运行时(不是编译时)是已知的。缓冲区大小永远不会改变。缓冲区被传递给其他例程以进行压缩、加密等:它只是一个字节序列,没有比这更高的了。

在 C 语言中,我会使用 malloc 分配缓冲区,然后在完成后分配 free。但是,我的代码是现代 C++,当然没有 mallocs,而且很少有原始的 newdelete:我正在大量使用 RAII 和 shared_ptr。然而,这些技术似乎都不适合这个缓冲区。它只是一个固定长度的字节缓冲区,用于接收 I/O 并使其内容可用。

是否有一个现代的 C++ 成语来优雅地表达这一点?或者,在这方面,我是否应该坚持使用好的 ol' malloc

【问题讨论】:

  • std::vector。始终std::vector。嗯...几乎除非你有很好的理由。
  • 从 C++11 开始,您甚至可以使用 .data() 访问向量后面的原始存储
  • @StefanoSanfilippo:总是可以使用&front()(仅限非空,授予)
  • 旁白:您是否考虑过将streambuf 子类化作为提供功能的手段?
  • 和其他人一样,我说使用std::vector

标签: c++ memory-management io


【解决方案1】:

基本上,您有两种主要的 C++ 方式选择:

  • std::vector
  • std::unique_ptr

我更喜欢第二个,因为您不需要 std::vector 中的所有自动调整大小的东西,而且您不需要容器 - 您只需要一个缓冲区。

std::unique_ptr 专门用于动态数组:std::unique_ptr<int[]> 将在其析构函数中调用delete [],并为您提供适当的operator []

如果你想要代码:

std::unique_ptr<char[]> buffer(new char [size]);
some_io_function(buffer.get(), size); // get() returnes raw pointer

不幸的是,它无法检索缓冲区的大小,因此您必须将其存储在变量中。如果它让您感到困惑,那么std::vector 将完成这项工作:

std::vector<char> buffer(size);
some_io_function(buffer.data(), buffer.size()); // data() returnes raw pointer

如果你想传递缓冲区,这取决于你是怎么做的。

考虑以下情况:缓冲区在某处填充,然后在其他地方处理,存储一段时间,然后在某处写入并销毁。碰巧你从来不需要代码中的两个地方来拥有缓冲区,你可以简单地std::move它从一个地方到另一个地方。对于这个用例,std::unique_ptr 可以很好地工作,并且可以防止您偶尔复制缓冲区(而使用std::vector 您可以错误地复制它,不会出现错误或警告)。

相反,如果您需要代码中的多个位置来保存同一个缓冲区(可能同时在多个位置填充/使用/处理它),您肯定需要std::shared_ptr。不幸的是,它没有类似数组的特化,所以你必须传递适当的删除器:

std::shared_ptr<char> buffer(new char[size], std::default_delete<char[]>());

第三个选项是如果你真的需要复制缓冲区。然后,std::vector 会更简单。但是,正如我已经提到的,我觉得这不是最好的方法。此外,您始终可以手动复制 std::unique_ptrstd::shared_ptr 的缓冲区,这清楚地记录了您的意图:

std::uniqure_ptr<char[]> buffer_copy(new char[size]);
std::copy(buffer.get(), buffer.get() + size, buffer_copy.get());

【讨论】:

  • 使用 std::unique_ptr 可能不是应该被重用并传递给多个函数的缓冲区的最佳选择。
  • @JoachimPileborg 在关于多个功能的问题中没有说什么。顺便说一句,根据我的经验,通常不以这种方式使用 IO 缓冲区。如果仍然需要它,那么 std::vector 也不起作用 - 例如,您必须使用 std::shared_ptr
  • "缓冲区被传递给其他例程..."。关于使用向量,至少它不是设计为主要通过值传递。
  • @JoachimPileborg 错过了那部分,我很抱歉。好吧,你总是可以移动std::unique_ptr,它还会保护你不要试图复制整个缓冲区(std::vector 会很乐意复制)。
  • 不知道std::unique_ptr 专门研究数组,很好。
【解决方案2】:

C++14 中,有一种语法非常简洁的方法可以实现您想要的:

size_t n = /* size of buffer */;
auto buf_ptr = std::make_unique<uint8_t[]>(n);
auto nr = ::read(STDIN_FILENO, buf_ptr.get(), n);
auto nw = ::write(STDOUT_FILENO, buf_ptr.get(), nr);
// etc.
// buffer is freed automatically when buf_ptr goes out of scope

请注意,上述构造将对缓冲区进行值初始化(清零)。如果您想跳过初始化以节省几个周期,则必须使用 lisyarus 给出的稍微难看的形式:

std::unique_ptr<uint8_t[]> buf_ptr(new uint8_t[n]);

C++20引入std::make_unique_for_overwrite,让上面的非初始化行可以更简洁的写成:

auto buf_ptr = std::make_unique_for_overwrite<uint8_t[]>(n);

【讨论】:

  • Matt 回答的另一个巨大优势是销毁时间有了很大的改善。我最近将一些代码从使用大向量作为缓冲区(销毁时间以秒为单位)转换为具有有效即时销毁的 unique_ptr。
  • @Nanki 难以置信;你能解释一下是什么原因造成的吗?
  • @JoeManiaci:小心你的术语。全局变量可以在全局命名空间以外的命名空间中定义。 :: 前缀专门表示在全局命名空间中定义的符号。我在这里用它来澄清我正在调用全局命名空间中定义的read(…)write(…) 函数,而不是一些成员函数。
  • @JoeManiaci:全局函数与全局变量并不完全相同。我使用术语“全局函数”作为“非成员函数”的同义词——即未定义为类型成员的函数。对于这些术语是否真的是同义词,似乎存在一些分歧。谷歌的 C++ 风格指南说:“最好将非成员函数放在命名空间中;很少使用完全全局的函数。”然而,Nell B. Dale 的“C++ Plus 数据结构”说,“全局范围是在所有函数和类之外声明的标识符的范围。”
  • std::make_unique_default_init 已重命名为 std::make_unique_for_overwriteSome more details关于功能。
【解决方案3】:

是的,很简单:

std::vector<char> myBuffer(N);

【讨论】:

    【解决方案4】:

    我认为为此使用std::vector 是很常见的。

    与手动分配的char 缓冲区相比,使用std::vector 的好处是复制语义(用于传递给希望为自己的目的修改数据的函数或将数据返回给调用函数时)。

    std::vector 也知道自己的大小,从而减少了需要传递给处理函数的参数数量并消除了错误源。

    您可以完全控制如何将数据传递给其他函数 - 可以通过 referenceconst reference 视情况而定。

    如果您需要使用普通的 char* 和长度调用旧的 c 样式 函数,您也可以轻松地做到这一点:

    // pass by const reference to preserve data
    void print_data(const std::vector<char>& buf)
    {
        std::cout.fill('0');
        std::cout << "0x";
        for(auto c: buf)
            std::cout << std::setw(2) << std::hex << int(c);
        std::cout << '\n';
    }
    
    // pass by reference to modify data
    void process_data(std::vector<char>& buf)
    {
        for(auto& c: buf)
            c += 1;
    }
    
    // pass by copy to modify data for another purpose
    void reinterpret_data(std::vector<char> buf)
    {
        // original data not changed
        process_data(buf);
        print_data(buf);
    }
    
    void legacy_function(const char* buf, std::size_t length)
    {
        // stuff
    }
    
    int main()
    {
        std::ifstream ifs("file.txt");
    
        // 24 character contiguous buffer
        std::vector<char> buf(24);
    
        while(ifs.read(buf.data(), buf.size()))
        {
            // changes data (pass by reference)
            process_data(buf);
    
            // modifies data internally (pass by value)
            reinterpret_data(buf);
    
            // non-modifying function (pass by const ref)
            print_data(buf);
    
            legacy_function(buf.data(), buf.size());
        }
    }
    

    【讨论】:

    • 我认为这对我没有帮助。向量需要传递给其他函数,所以我需要用 new 分配它然后释放它。此外,向量真的是为这种类型的 I/O 设计的吗?数据块有保障吗?
    • 是的,std::vectors 存储被指定为连续的。并且由于移动语义,通过值传递向量也很有效,不需要引用语义(因此不需要内存管理)
    • @SRobertJames 您无需动态分配 vector 即可将其传递给其他函数。通常你通过 referenceconst reference 传递。我已经更新了答案以演示用法。 std::vector 也保证是一个连续的块。
    • @SRobertJames 我不明白为什么你会在将它传递给其他函数时遇到任何问题 - 只需通过引用传递它 - 如果存在生命周期问题,你应该提及它们。
    • "数据块有保障吗?"我假设您想要一块内存来进行批量读取?将您需要的大小作为参数传递给显示的构造函数,以预分配缓冲区并将其初始化为 0。构造函数的另一种形式允许您除了传递大小之外还传递一个要初始化的值,例如std::vector&lt;char&gt; buf(24, 0xff);
    【解决方案5】:

    使用

    std::vector<char> buffer(N)
    

    如果缓冲区的大小永远不会改变,您可以通过这样做将其用作数组:

    char * bufPtr = &buffer[0];
    

    这也适用于 C++03。请参阅此评论 https://stackoverflow.com/a/247764/1219722 了解为什么这是安全的详细信息。

    【讨论】:

      猜你喜欢
      • 2015-08-20
      • 1970-01-01
      • 2015-07-05
      • 2018-12-21
      • 1970-01-01
      • 1970-01-01
      • 2016-06-04
      • 2014-09-20
      • 2012-01-14
      相关资源
      最近更新 更多