用于分配/释放 I/O 缓冲区的现代 C++ 习惯用法
Modern C++ idiom for allocating / deallocating an I/O buffer
为了 I/O 工作,我需要将 N 个字节读入缓冲区。 N 在 运行 时已知(不是编译时)。缓冲区大小永远不会改变。缓冲区被传递给其他例程以进行压缩、加密等:它只是一个字节序列,仅此而已。
在 C 中,我会使用 malloc
分配缓冲区,然后在完成后使用 free
分配缓冲区。但是,我的代码是现代 C++,当然没有 malloc
,而且原始 new
和 delete
很少:我大量使用 RAII 和 shared_ptr
。然而,None 这些技术似乎适用于此缓冲区。它只是一个固定长度的字节缓冲区,用于接收 I/O 并使其内容可用。
是否有一个现代的 C++ 习语来优雅地表达这一点?或者,就这方面而言,我应该坚持使用 good ol' malloc
?
是的,简单:
std::vector<char> myBuffer(N);
我认为为此使用 std::vector 很常见。
在手动分配的 char
缓冲区上使用 std::vector 的好处是复制语义(用于传递给希望为自己的目的修改数据的函数,或者在将数据返回给调用时功能)。
std::vector 也知道自己的大小,减少了需要传递给处理函数的参数数量并消除了错误来源。
您可以完全控制数据如何传递给其他函数 - 通过 reference 或 const 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());
}
}
基本上,您有两个主要的 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_ptr
或 std::shared_ptr
手动复制缓冲区保持,这清楚地记录了您的意图:
std::uniqure_ptr<char[]> buffer_copy(new char[size]);
std::copy(buffer.get(), buffer.get() + size, buffer_copy.get());
使用
std::vector<char> buffer(N)
如果缓冲区的大小永远不变,您可以通过这样做将其用作数组:
char * bufPtr = &buffer[0];
这也适用于 C++03。请参阅此评论 以详细了解为什么这是安全的。
在 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);
为了 I/O 工作,我需要将 N 个字节读入缓冲区。 N 在 运行 时已知(不是编译时)。缓冲区大小永远不会改变。缓冲区被传递给其他例程以进行压缩、加密等:它只是一个字节序列,仅此而已。
在 C 中,我会使用 malloc
分配缓冲区,然后在完成后使用 free
分配缓冲区。但是,我的代码是现代 C++,当然没有 malloc
,而且原始 new
和 delete
很少:我大量使用 RAII 和 shared_ptr
。然而,None 这些技术似乎适用于此缓冲区。它只是一个固定长度的字节缓冲区,用于接收 I/O 并使其内容可用。
是否有一个现代的 C++ 习语来优雅地表达这一点?或者,就这方面而言,我应该坚持使用 good ol' malloc
?
是的,简单:
std::vector<char> myBuffer(N);
我认为为此使用 std::vector 很常见。
在手动分配的 char
缓冲区上使用 std::vector 的好处是复制语义(用于传递给希望为自己的目的修改数据的函数,或者在将数据返回给调用时功能)。
std::vector 也知道自己的大小,减少了需要传递给处理函数的参数数量并消除了错误来源。
您可以完全控制数据如何传递给其他函数 - 通过 reference 或 const 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());
}
}
基本上,您有两个主要的 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_ptr
或 std::shared_ptr
手动复制缓冲区保持,这清楚地记录了您的意图:
std::uniqure_ptr<char[]> buffer_copy(new char[size]);
std::copy(buffer.get(), buffer.get() + size, buffer_copy.get());
使用
std::vector<char> buffer(N)
如果缓冲区的大小永远不变,您可以通过这样做将其用作数组:
char * bufPtr = &buffer[0];
这也适用于 C++03。请参阅此评论 以详细了解为什么这是安全的。
在 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);