使用 std::string 作为缓冲区有缺点吗?
Are there downsides to using std::string as a buffer?
我最近看到我的一位同事使用 std::string
作为缓冲区:
std::string receive_data(const Receiver& receiver) {
std::string buff;
int size = receiver.size();
if (size > 0) {
buff.resize(size);
const char* dst_ptr = buff.data();
const char* src_ptr = receiver.data();
memcpy((char*) dst_ptr, src_ptr, size);
}
return buff;
}
我猜这家伙想利用返回字符串的自动销毁,这样他就不必担心释放分配的缓冲区。
这对我来说有点奇怪,因为根据 cplusplus.com data()
方法 returns 一个 const char*
指向由字符串内部管理的缓冲区:
const char* data() const noexcept;
Memcpy 到 const char 指针? AFAIK 只要我们知道我们在做什么,这就没有害处,但我错过了什么吗?这很危险吗?
从 C++17 开始,data
可以 return 一个非常量 char *
。
n4659 草案声明于 [string.accessors]:
const charT* c_str() const noexcept;
const charT* data() const noexcept;
....
charT* data() noexcept;
您可以通过调用适当的构造函数完全避免手动 memcpy
:
std::string receive_data(const Receiver& receiver) {
return {receiver.data(), receiver.size()};
}
甚至可以处理字符串中的 [=12=]
。
顺便说一句,除非内容实际上是文本,否则我更喜欢 std::vector<std::byte>
(或等效)。
代码是不必要的,考虑到
std::string receive_data(const Receiver& receiver) {
std::string buff;
int size = receiver.size();
if (size > 0) {
buff.assign(receiver.data(), size);
}
return buff;
}
将完全相同。
不要使用 std::string
作为缓冲区。
使用 std::string
作为缓冲区是一种不好的做法,原因有几个(排名不分先后):
std::string
并非旨在用作缓冲区;您需要 double-check class 的描述以确保没有 "gotchas" 会阻止某些使用模式(或使它们触发未定义的行为)。
- 举个具体的例子:在 C++17 之前,你 can't even write 通过你用
data()
得到的指针——它是 const Tchar *
;所以你的代码会导致未定义的行为。 (但是 &(str[0])
、&(str.front())
或 &(*(str.begin()))
会起作用。)
- 将
std::string
用于缓冲区会使阅读函数定义的读者感到困惑,他们认为您将使用 std::string
来表示字符串。换句话说,这样做会破坏 Principle of Least Astonishment.
- 更糟糕的是,这会让可能 使用 你的函数的人感到困惑 - 他们也可能认为你返回的是一个字符串,即有效的 human-readable 文本。
std::unique_ptr
would be fine for your case, or even std::vector
. In C++17, you can use std::byte
for the element type, too. A more sophisticated option is a class with an SSO-like feature, e.g. Boost's small_vector
(谢谢 @gast128 提到它)。
- (次要点:)libstdc++ 必须更改其 ABI
std::string
以符合 C++11 标准,因此在某些情况下(现在不太可能),您可能 运行 进入一些链接或 运行 时间 issues,你不会为你的缓冲区使用不同的类型。
此外,您的代码可能会进行两次而不是一次堆分配(取决于实现):一次是字符串构造,一次是 resize()
ing。但这本身并不是避免 std::string
的真正原因,因为您可以使用 .
中的构造来避免双重分配
Memcpy-ing to a const char pointer? AFAIK this does no harm as long as we know what we do, but is this good behavior and why?
当前代码可能有未定义的行为,具体取决于 C++ 版本。为避免 C++14 及以下版本中的未定义行为,请获取第一个元素的地址。它产生一个 non-const 指针:
buff.resize(size);
memcpy(&buff[0], &receiver[0], size);
I have recently seen a colleague of mine using std::string
as a buffer...
这在旧代码中有些常见,尤其是大约 C++03。使用这样的字符串有几个优点和缺点。根据您对代码所做的操作,std::vector
可能有点贫血,您有时会使用字符串代替并接受 char_traits
.
的额外开销
例如,std::string
通常是比 std::vector
追加更快的容器,并且您不能从函数中 return std::vector
。 (或者在 C++98 中你不能在实践中这样做,因为 C++98 要求在函数中构造向量并复制出来)。此外,std::string
允许您使用更丰富的成员函数进行搜索,例如 find_first_of
和 find_first_not_of
。这在搜索字节数组时很方便。
我想你真正want/need是SGI的Rope class, but it never made it into the STL. It looks like GCC's libstdc++可能会提供。
关于这在 C++14 及以下版本中是否合法的讨论很长:
const char* dst_ptr = buff.data();
const char* src_ptr = receiver.data();
memcpy((char*) dst_ptr, src_ptr, size);
我确定它在 GCC 中不安全。我曾经在一些自我测试中做过这样的事情,结果导致了段错误:
std::string buff("A");
...
char* ptr = (char*)buff.data();
size_t len = buff.size();
ptr[0] ^= 1; // tamper with byte
bool tampered = HMAC(key, ptr, len, mac);
GCC 将单个字节 'A'
放入寄存器 AL
。高 3 字节是垃圾,所以 32 位寄存器是 0xXXXXXX41
。当我在 ptr[0]
处取消引用时,GCC 取消引用垃圾地址 0xXXXXXX41
.
我的两个take-aways是,不要写half-ass自我测试,也不要试图使data()
成为non-const指针。
我在这里要研究的最大优化机会是:Receiver
似乎是某种支持 .data()
和 .size()
的容器。如果您可以使用它,并将其作为右值引用 Receiver&&
传递,您也许可以使用移动语义,而无需制作任何副本!如果它有一个迭代器接口,您可以将它们用于 range-based 构造函数或 <algorithm>
.
中的 std::move()
在 C++17 中(如 Serge Ballesta 和其他人所提到的),std::string::data()
returns 指向 non-const 数据的指针。 std::string
已保证连续存储其所有数据多年。
写的代码有点味道,虽然这并不是程序员的错:那些 hack 在当时是必要的。今天,您至少应该将 dst_ptr
的类型从 const char*
更改为 char*
,并将第一个参数中的类型转换为 memcpy()
。您还可以 reserve()
缓冲区的字节数,然后使用 STL 函数移动数据。
正如其他人所提到的,std::vector
或 std::unique_ptr
将是更自然的数据结构。
一个缺点是性能。
.resize 方法会将 default-initialize 所有新字节位置设为 0。
如果您随后要用其他数据覆盖 0,则不需要进行初始化。
我确实觉得 std::string
是管理“缓冲区”的合法 竞争者;它是否是最佳选择取决于一些因素...
您的缓冲区内容本质上是文本的还是二进制的?
您的决定的一个主要输入应该是缓冲区内容是否本质上是文本。如果将 std::string
用于文本内容,代码的读者可能不会感到困惑。
char
不是存储字节的好类型。 请记住,C++ 标准让每个实现决定是否 char
将被签名或未签名,但对于二进制数据的通用黑盒处理(有时甚至将字符传递给具有未定义行为的函数 std::toupper(int)
时,除非参数在 unsigned char
范围内或等于EOF
) 你可能想要无符号数据:如果它是不透明的二进制数据,你为什么要假设或暗示每个字节的第一位是符号位?
正因为如此,不可否认,将 std::string
用于“二进制”数据 有点 hackish。您可以使用 std::basic_string<std::byte>
,但这不是问题所要问的,并且您会因使用无处不在的 std::string
类型而失去一些不可操作性的好处。
使用 std::string 的一些潜在好处
首先是几个好处:
它体现了我们都知道和喜爱的 RAII 语义
大多数实现都具有短字符串优化 (SSO) 功能,这确保如果字节数小到足以直接放入字符串对象,则可以避免动态 allocation/deallocation(但每次访问数据时可能会多出一个分支)
- 这对于传递读取或写入的数据副本可能更有用,而不是对于缓冲区,缓冲区应预先调整大小以在可用时接受合适的数据块(通过处理更多 [来提高吞吐量) =128=]一次)
有大量的 std::string
成员函数和非成员函数,旨在与 std::string
s(包括例如 cout << my_string
)配合使用:如果您客户端代码会发现它们对 parse/manipulate/process 缓冲区内容很有用,然后你就可以顺利开始了
大多数 C++ 程序员都非常熟悉 API
喜忧参半
- 作为一种熟悉的、无处不在的类型,您与之交互的代码可能具有针对
std::string
的特化,更适合您对缓冲数据的使用,或者这些特化可能更差:评估
关心
正如 Waxrat 所观察到的,API 明智地缺乏有效增加缓冲区的能力,因为 resize()
将 NULs/'\0's 写入添加的字符,如果你即将“接收”值到该内存中。这与正在制作接收数据副本且大小已知的 OP 代码无关。
讨论
解决 einpoklum 的问题:
std::string
was not intended for use as a buffer; you would need to double-check the description of the class to make sure there are no "gotchas" which would prevent certain usage patterns (or make them trigger undefined behavior).
虽然 std::string
最初并非为此目的,但其余的主要是 FUD。标准通过 C++17 的非 const
成员函数 char* data()
对这种用法做出了让步,并且 string
始终支持嵌入的零字节。大多数高级程序员都知道做什么是安全的。
备选方案
静态缓冲区(C char[N]
数组或 std::array<char, N>
)大小为某个最大消息大小,或每次调用传送数据片段
手动分配缓冲区 std::unique_ptr
以自动销毁:让您进行精细的大小调整,并自行跟踪分配的大小与使用中的大小;总体上更容易出错
std::vector
(对于元素类型可能是 std::byte
;被广泛理解为暗示二进制数据,但 API 更具限制性并且(为了更好或更糟)它不能期望有任何等同于短字符串优化的东西。
Boost 的 small_vector
:也许,如果 SSO 是唯一阻碍您使用 std::vector
的因素,并且您很乐意使用 boost。
返回一个允许延迟访问接收到的数据的仿函数(前提是您知道它不会被释放或覆盖),推迟客户端代码选择如何存储它
我最近看到我的一位同事使用 std::string
作为缓冲区:
std::string receive_data(const Receiver& receiver) {
std::string buff;
int size = receiver.size();
if (size > 0) {
buff.resize(size);
const char* dst_ptr = buff.data();
const char* src_ptr = receiver.data();
memcpy((char*) dst_ptr, src_ptr, size);
}
return buff;
}
我猜这家伙想利用返回字符串的自动销毁,这样他就不必担心释放分配的缓冲区。
这对我来说有点奇怪,因为根据 cplusplus.com data()
方法 returns 一个 const char*
指向由字符串内部管理的缓冲区:
const char* data() const noexcept;
Memcpy 到 const char 指针? AFAIK 只要我们知道我们在做什么,这就没有害处,但我错过了什么吗?这很危险吗?
从 C++17 开始,data
可以 return 一个非常量 char *
。
n4659 草案声明于 [string.accessors]:
const charT* c_str() const noexcept; const charT* data() const noexcept; .... charT* data() noexcept;
您可以通过调用适当的构造函数完全避免手动 memcpy
:
std::string receive_data(const Receiver& receiver) {
return {receiver.data(), receiver.size()};
}
甚至可以处理字符串中的 [=12=]
。
顺便说一句,除非内容实际上是文本,否则我更喜欢 std::vector<std::byte>
(或等效)。
代码是不必要的,考虑到
std::string receive_data(const Receiver& receiver) {
std::string buff;
int size = receiver.size();
if (size > 0) {
buff.assign(receiver.data(), size);
}
return buff;
}
将完全相同。
不要使用 std::string
作为缓冲区。
使用 std::string
作为缓冲区是一种不好的做法,原因有几个(排名不分先后):
std::string
并非旨在用作缓冲区;您需要 double-check class 的描述以确保没有 "gotchas" 会阻止某些使用模式(或使它们触发未定义的行为)。- 举个具体的例子:在 C++17 之前,你 can't even write 通过你用
data()
得到的指针——它是const Tchar *
;所以你的代码会导致未定义的行为。 (但是&(str[0])
、&(str.front())
或&(*(str.begin()))
会起作用。) - 将
std::string
用于缓冲区会使阅读函数定义的读者感到困惑,他们认为您将使用std::string
来表示字符串。换句话说,这样做会破坏 Principle of Least Astonishment. - 更糟糕的是,这会让可能 使用 你的函数的人感到困惑 - 他们也可能认为你返回的是一个字符串,即有效的 human-readable 文本。
std::unique_ptr
would be fine for your case, or evenstd::vector
. In C++17, you can usestd::byte
for the element type, too. A more sophisticated option is a class with an SSO-like feature, e.g. Boost'ssmall_vector
(谢谢 @gast128 提到它)。- (次要点:)libstdc++ 必须更改其 ABI
std::string
以符合 C++11 标准,因此在某些情况下(现在不太可能),您可能 运行 进入一些链接或 运行 时间 issues,你不会为你的缓冲区使用不同的类型。
此外,您的代码可能会进行两次而不是一次堆分配(取决于实现):一次是字符串构造,一次是 resize()
ing。但这本身并不是避免 std::string
的真正原因,因为您可以使用
Memcpy-ing to a const char pointer? AFAIK this does no harm as long as we know what we do, but is this good behavior and why?
当前代码可能有未定义的行为,具体取决于 C++ 版本。为避免 C++14 及以下版本中的未定义行为,请获取第一个元素的地址。它产生一个 non-const 指针:
buff.resize(size);
memcpy(&buff[0], &receiver[0], size);
I have recently seen a colleague of mine using
std::string
as a buffer...
这在旧代码中有些常见,尤其是大约 C++03。使用这样的字符串有几个优点和缺点。根据您对代码所做的操作,std::vector
可能有点贫血,您有时会使用字符串代替并接受 char_traits
.
例如,std::string
通常是比 std::vector
追加更快的容器,并且您不能从函数中 return std::vector
。 (或者在 C++98 中你不能在实践中这样做,因为 C++98 要求在函数中构造向量并复制出来)。此外,std::string
允许您使用更丰富的成员函数进行搜索,例如 find_first_of
和 find_first_not_of
。这在搜索字节数组时很方便。
我想你真正want/need是SGI的Rope class, but it never made it into the STL. It looks like GCC's libstdc++可能会提供。
关于这在 C++14 及以下版本中是否合法的讨论很长:
const char* dst_ptr = buff.data();
const char* src_ptr = receiver.data();
memcpy((char*) dst_ptr, src_ptr, size);
我确定它在 GCC 中不安全。我曾经在一些自我测试中做过这样的事情,结果导致了段错误:
std::string buff("A");
...
char* ptr = (char*)buff.data();
size_t len = buff.size();
ptr[0] ^= 1; // tamper with byte
bool tampered = HMAC(key, ptr, len, mac);
GCC 将单个字节 'A'
放入寄存器 AL
。高 3 字节是垃圾,所以 32 位寄存器是 0xXXXXXX41
。当我在 ptr[0]
处取消引用时,GCC 取消引用垃圾地址 0xXXXXXX41
.
我的两个take-aways是,不要写half-ass自我测试,也不要试图使data()
成为non-const指针。
我在这里要研究的最大优化机会是:Receiver
似乎是某种支持 .data()
和 .size()
的容器。如果您可以使用它,并将其作为右值引用 Receiver&&
传递,您也许可以使用移动语义,而无需制作任何副本!如果它有一个迭代器接口,您可以将它们用于 range-based 构造函数或 <algorithm>
.
std::move()
在 C++17 中(如 Serge Ballesta 和其他人所提到的),std::string::data()
returns 指向 non-const 数据的指针。 std::string
已保证连续存储其所有数据多年。
写的代码有点味道,虽然这并不是程序员的错:那些 hack 在当时是必要的。今天,您至少应该将 dst_ptr
的类型从 const char*
更改为 char*
,并将第一个参数中的类型转换为 memcpy()
。您还可以 reserve()
缓冲区的字节数,然后使用 STL 函数移动数据。
正如其他人所提到的,std::vector
或 std::unique_ptr
将是更自然的数据结构。
一个缺点是性能。 .resize 方法会将 default-initialize 所有新字节位置设为 0。 如果您随后要用其他数据覆盖 0,则不需要进行初始化。
我确实觉得 std::string
是管理“缓冲区”的合法 竞争者;它是否是最佳选择取决于一些因素...
您的缓冲区内容本质上是文本的还是二进制的?
您的决定的一个主要输入应该是缓冲区内容是否本质上是文本。如果将 std::string
用于文本内容,代码的读者可能不会感到困惑。
char
不是存储字节的好类型。 请记住,C++ 标准让每个实现决定是否 char
将被签名或未签名,但对于二进制数据的通用黑盒处理(有时甚至将字符传递给具有未定义行为的函数 std::toupper(int)
时,除非参数在 unsigned char
范围内或等于EOF
) 你可能想要无符号数据:如果它是不透明的二进制数据,你为什么要假设或暗示每个字节的第一位是符号位?
正因为如此,不可否认,将 std::string
用于“二进制”数据 有点 hackish。您可以使用 std::basic_string<std::byte>
,但这不是问题所要问的,并且您会因使用无处不在的 std::string
类型而失去一些不可操作性的好处。
使用 std::string 的一些潜在好处
首先是几个好处:
它体现了我们都知道和喜爱的 RAII 语义
大多数实现都具有短字符串优化 (SSO) 功能,这确保如果字节数小到足以直接放入字符串对象,则可以避免动态 allocation/deallocation(但每次访问数据时可能会多出一个分支)
- 这对于传递读取或写入的数据副本可能更有用,而不是对于缓冲区,缓冲区应预先调整大小以在可用时接受合适的数据块(通过处理更多 [来提高吞吐量) =128=]一次)
有大量的
std::string
成员函数和非成员函数,旨在与std::string
s(包括例如cout << my_string
)配合使用:如果您客户端代码会发现它们对 parse/manipulate/process 缓冲区内容很有用,然后你就可以顺利开始了大多数 C++ 程序员都非常熟悉 API
喜忧参半
- 作为一种熟悉的、无处不在的类型,您与之交互的代码可能具有针对
std::string
的特化,更适合您对缓冲数据的使用,或者这些特化可能更差:评估
关心
正如 Waxrat 所观察到的,API 明智地缺乏有效增加缓冲区的能力,因为 resize()
将 NULs/'\0's 写入添加的字符,如果你即将“接收”值到该内存中。这与正在制作接收数据副本且大小已知的 OP 代码无关。
讨论
解决 einpoklum 的问题:
std::string
was not intended for use as a buffer; you would need to double-check the description of the class to make sure there are no "gotchas" which would prevent certain usage patterns (or make them trigger undefined behavior).
虽然 std::string
最初并非为此目的,但其余的主要是 FUD。标准通过 C++17 的非 const
成员函数 char* data()
对这种用法做出了让步,并且 string
始终支持嵌入的零字节。大多数高级程序员都知道做什么是安全的。
备选方案
静态缓冲区(C
char[N]
数组或std::array<char, N>
)大小为某个最大消息大小,或每次调用传送数据片段手动分配缓冲区
std::unique_ptr
以自动销毁:让您进行精细的大小调整,并自行跟踪分配的大小与使用中的大小;总体上更容易出错std::vector
(对于元素类型可能是std::byte
;被广泛理解为暗示二进制数据,但 API 更具限制性并且(为了更好或更糟)它不能期望有任何等同于短字符串优化的东西。Boost 的
small_vector
:也许,如果 SSO 是唯一阻碍您使用std::vector
的因素,并且您很乐意使用 boost。返回一个允许延迟访问接收到的数据的仿函数(前提是您知道它不会被释放或覆盖),推迟客户端代码选择如何存储它