【问题标题】:Are there downsides to using std::string as a buffer?使用 std::string 作为缓冲区有缺点吗?
【发布时间】:2019-10-18 17:54:24
【问题描述】:

我最近看到我的一个同事使用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() 方法返回一个const char* 指向一个由字符串内部管理的缓冲区:

const char* data() const noexcept;

Memcpy-ing 到一个 const char 指针? AFAIK 只要我们知道自己在做什么,这并没有什么坏处,但是我错过了什么吗?这很危险吗?

【问题讨论】:

  • 使用 C++17 data() 有一个重载,返回一个指向非 const 限定字符的指针。
  • ...cppreference 提到的。 cplusplus.com 不是最好的来源。
  • 我认为从const char*char* 的转换操作本身就意味着您的程序中存在某种危险。如果dst_ptr 指向一个只读内存块,那么您不应该尝试使用该指针写入该块。
  • 每当您看到使用 C 风格转换的代码(例如 (char*) dst_ptr)时,您都应该将其视为危险信号。
  • 我认为这个问题主要是基于意见的。如果您知道您正在接收文本数据,IMO 使用 std::string 作为缓冲区就可以了。如果您正在接收二进制数据,std::vector<char> 可能是更好的选择。

标签: c++ c++11 stdstring


【解决方案1】:

不要将std::string 用作缓冲区。

使用std::string 作为缓冲区是一种不好的做法,原因有几个(不按特定顺序列出):

  • std::string 不打算用作缓冲区;您需要仔细检查类的描述,以确保没有“陷阱”会阻止某些使用模式(或使它们触发未定义的行为)。
  • 作为一个具体的例子:在C++17之前,你通过data()得到的指针can't even write——它是const Tchar *;所以你的代码会导致未定义的行为。 (但&(str[0])&(str.front())&(*(str.begin())) 可以。)
  • std::strings 用于缓冲区会使函数定义的读者感到困惑,他们会假设您将std::string 用于字符串。换句话说,这样做会破坏Principle of Least Astonishment
  • 更糟糕的是,这会让任何可能使用您的函数的人感到困惑 - 他们也可能认为您返回的是一个字符串,即有效的人类可读文本。
  • std::unique_ptr 适合您的情况,甚至是 std::vector。在 C++17 中,您也可以使用 std::byte 作为元素类型。更复杂的选项是具有SSO-like 功能的类,例如Boost 的 small_vector(感谢 @gast128 提及)。
  • (次要观点:)libstdc++ 必须将其 ABI 更改为 std::string 以符合 C++11 标准,因此在某些情况下(目前不太可能),您可能会遇到一些链接或运行时 @ 987654328@ 你不会为你的缓冲区使用不同的类型。

此外,您的代码可能会进行两次而不是一次堆分配(取决于实现):一次是在字符串构造时,另一次是在resize()ing 时。但这本身并不是避免使用std::string 的真正理由,因为您可以使用@Jarod42's answer 中的构造来避免双重分配。

【讨论】:

  • 通过std::string_view,您可以定义缓冲区的可读部分。
  • @M.M 例如\0 保证存在的终止符std::string。您是否会立即知道它是否包含在data() 的有效范围内? Overwriting it is UB!它需要一些额外的心理周期(可能还有参考查找)来验证这个函数没有 UB。
  • @M.M:重点是——你并不凭直觉知道!我什至不知道 MaxLanghof 在他的评论中写了什么。
  • +1 为std::byte,难以置信我以前从未听说过!每天查看一些 C++ 参考资料的频率真是太疯狂了,而且新事物不断涌现……
  • @einpoklum:请注意,它不仅在创建中,而且在访问模式中。对于 SSO,一个可能具有现代处理器优化的内存访问模式的局部性。 boost 提供了 small_vector,它是一个带有 SSO 的向量。可惜这些东西最终没有出现在标准中。
【解决方案2】:

您可以通过调用适当的构造函数来完全避免手动memcpy

std::string receive_data(const Receiver& receiver) {
    return {receiver.data(), receiver.size()};
}

它甚至可以处理字符串中的\0

顺便说一句,除非内容实际上是文本,否则我更喜欢std::vector<std::byte>(或等效项)。

【讨论】:

  • 因为原始缓冲区数据不应该不是const,所以你不应该有UB。
  • 到那时一个优秀的程序员会说:“为什么我需要一个单独的单行转换函数?不仅如此,这个名为'receive'的函数实际上并没有执行任何'receiving '. 删除!”
  • @screwnut:公平地说,理论上receiver.data() 可能会等待接收发生,或者在返回成员指针之外做其他事情。
  • @screwnut:那我是个糟糕的程序员;我会将转换保存在一个单独的函数中,即使它是单行的,因为我欣赏抽象并且不喜欢重复自己。如果我以后需要添加一些检查、一些日志记录等......该功能就在这里,我不必在代码库中寻找所有转换实例。
  • @screwnut:首选非会员非好友功能。
【解决方案3】:

Memcpy-ing 到一个 const char 指针? AFAIK 只要我们知道自己在做什么,这并没有什么害处,但这是一种好的行为吗?为什么?

当前代码可能具有未定义的行为,具体取决于 C++ 版本。为避免 C++14 及更低版本中的未定义行为,请获取第一个元素的地址。它产生一个非常量指针:

buff.resize(size);
memcpy(&buff[0], &receiver[0], size);

我最近看到我的一个同事使用std::string 作为缓冲区...

这在旧代码中有些常见,尤其是在 C++03 左右。使用这样的字符串有几个优点和缺点。根据您对代码所做的操作,std::vector 可能会有点乏力,您有时会使用字符串代替并接受char_traits 的额外开销。

例如,std::string 在追加时通常是比std::vector 更快的容器,并且您不能从函数返回std::vector。 (或者在 C++98 中你不能在实践中这样做,因为 C++98 需要在函数中构造向量并复制出来)。此外,std::string 允许您使用更丰富的成员函数进行搜索,例如 find_first_offind_first_not_of。这在搜索字节数组时很方便。

我认为您真正想要/需要的是 SGI 的 Rope class,但它从未进入 STL。看起来 GCC 的 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

对我来说,两个要点是,不要写半途而废的自测,也不要试图让 data() 成为非常量指针。

【讨论】:

  • 首选std::copy 以保证类型安全。它不会更慢。
  • 似乎是唯一直接回答问题的答案。
  • “你不能从函数中返回 std::vector。(或者你不能在 C++98 或 C++03 中这样做)”是错误的。
  • 编译器将地址与存储的内容混淆从来都不是合法的优化。 buff.data() 不能是包含'A' 的寄存器,它必须是地址。
  • @jww:确实,尽管您可以通过 NRVO 和swap() 调用来避免该副本。但也需要std::string 的副本。小字符串优化可以使它更好一点。我认为一些实现尝试使用写时复制来解决这个问题(对于string,从不允许vector),但即使在C++98和C++03中,std::string上的一些规范也不能COW 不合理地满足。当然,右值引用和移动巧妙地解决了它。
【解决方案4】:

从 C++17 开始,data 可以返回非 const char *

草案 n4659 在 [string.accessors] 处声明:

const charT* c_str() const noexcept;
const charT* data() const noexcept;
....
charT* data() noexcept;

【讨论】:

  • @SergeBallesta - 删除 const 限定符 不是 UB。修改一个 const 对象是 UB。有问题的对象不是 const。
  • @SergeBallesta - 认真的吗?你如何协调&str[0] 是指向同一个缓冲区的非常量指针?该对象保证不是 const。该语言的核心规则仍然适用,即使是从库类型返回的指针,因此,没有 UB。
  • @Jarod42:我同意我在这里吹毛求疵,但库可能期望缓冲区不会被更改,然后重用缓存版本。对于旧的 K&R C,我现在对优化编译器感到害怕,并且对 constness 和严格的别名非常谨慎。
  • @StoryTeller Serge 是正确的,“修改通过数据的 const 重载访问的字符数组具有未定义的行为。” 根据cppreferencethe standard。跨度>
  • (为了完整起见,这里是 C++11 的措辞:timsong-cpp.github.io/cppwp/n3337/string.ops#string.accessors-3
【解决方案5】:

代码是不必要的,考虑到

std::string receive_data(const Receiver& receiver) {
    std::string buff;
    int size = receiver.size();
    if (size > 0) {
        buff.assign(receiver.data(), size);
    }
    return buff;
}

会做同样的事情。

【讨论】:

  • 你可以剪切更多的代码; if 也是不必要的。 assign 将是一个空操作。但是继续删除不必要的代码,你最终会得到 Jarod42 的答案。 没有这些行是必需的,因为std::string 已经有一个适当的构造函数。
  • @MSalters 我不想假设没有给出的东西。如果receiver.size() 可以返回负值怎么办?
  • 这将是相当出乎意料的,因为大小通常是 size_t,因此是未签名的。这确实表明您的代码可能存在问题:它可能会遭受有符号整数溢出,这是未定义的行为。那是在处理输入的代码路径上,所以这可能构成一个外部可利用的漏洞。
  • @MSalters 没错,Jarod42 的更改可能会引入外部可利用的漏洞。如果receiver.data() 是UB 而receiver.size() 为零,它们也可能会导致崩溃。
【解决方案6】:

我将在这里调查的最大优化机会是:Receiver 似乎是某种支持.data().size() 的容器。如果您可以使用它,并将其作为右值引用 Receiver&& 传递,您可能可以使用移动语义而无需制作任何副本!如果它有一个迭代器接口,你可以将它们用于基于范围的构造函数或来自<algorithm>std::move()

在 C++17 中(正如 Serge Ballesta 和其他人所提到的),std::string::data() 返回一个指向非常量数据的指针。 std::string 已保证将其所有数据连续存储多年。

写出来的代码有点味道,虽然这并不是程序员的错:当时那些黑客是必要的。今天,您至少应该将dst_ptr 的类型从const char* 更改为char*,并删除memcpy() 的第一个参数中的强制转换。您也可以reserve() 缓冲区的字节数,然后使用 STL 函数来移动数据。

正如其他人所提到的,std::vectorstd::unique_ptr 将是在这里使用的更自然的数据结构。

【讨论】:

    【解决方案7】:

    一个缺点是性能。 .resize 方法会将所有新字节位置默认初始化为 0。 如果您随后要用其他数据覆盖 0,则无需进行初始化。

    【讨论】:

      【解决方案8】:

      我确实觉得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),它确保如果字节数足够小以直接放入字符串对象中,则可以避免动态分配/释放(但每次可能会有一个额外的分支数据被访问)

        • 这对于传递读取或写入的数据副本可能更有用,而不是对于应该预先调整大小以接受适当数据块(如果可用)的缓冲区(通过处理更多 I/O 来提高吞吐量)更有用一次)
      • 有大量的std::string 成员函数和非成员函数,它们旨在与std::strings(包括例如cout << my_string)一起工作:如果您的客户端代码发现它们对解析/操作/处理有用缓冲内容,然后您就可以开始了

      • 大多数 C++ 程序员都非常熟悉该 API

      喜忧参半

      • 作为一种熟悉的、普遍存在的类型,您与之交互的代码可能具有针对 std::string 的特化,更适合您用于缓冲数据,或者这些特化可能更糟:请评估一下

      关心

      正如 Waxrat 所观察到的,API 方面缺乏有效增长缓冲区的能力,因为resize() 将 NULs/'\0's 写入添加的字符中,如果您要“接收”值则毫无意义记忆。这与 OPs 代码无关,其中正在制作接收数据的副本,并且大小是已知的。

      讨论

      解决 einpoklum 的担忧:

      std::string 不打算用作缓冲区;您需要仔细检查类的描述,以确保没有“陷阱”会阻止某些使用模式(或使它们触发未定义的行为)。

      虽然std::string 最初并不是为此而设计的,但其余的主要是 FUD。标准通过 C++17 的非const 成员函数char* data() 对这种用法做出了让步,string 一直支持嵌入零字节。大多数高级程序员都知道什么是安全的。

      替代方案

      • 静态缓冲区(C char[N] 数组或 std::array<char, N>)大小为某个最大消息大小,或每次调用传送数据切片

      • 带有std::unique_ptr 的手动分配缓冲区以自动销毁:让您可以进行繁琐的调整大小,并自己跟踪分配的大小与使用中的大小;总体上更容易出错

      • std::vector(对于元素类型可能是 std::byte;被广泛理解为暗示二进制数据,但 API 更具限制性,并且(无论好坏)不能指望它有任何等同于 Short -字符串优化。

      • Boost 的small_vector:也许,如果 SSO 是唯一让您远离std::vector 的因素,并且您很高兴使用 boost。

      • 返回一个允许对接收到的数据进行延迟访问的仿函数(前提是您知道它不会被释放或覆盖),延迟选择由客户端代码存储的方式

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-06-16
        • 2016-12-16
        • 1970-01-01
        • 2017-01-05
        • 1970-01-01
        • 2012-01-01
        相关资源
        最近更新 更多