使用std :: string作为缓冲区有不利之处吗?

时间:2019-06-03 07:33:49

标签: c++ c++11 stdstring

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

const char* data() const noexcept;

存储到const char指针? AFAIK只要知道我们所做的事情就不会造成伤害,但是我错过了什么吗?这很危险吗?

8 个答案:

答案 0 :(得分:69)

请勿使用std::string作为缓冲区。

出于某些原因(以不特定的顺序列出),使用std::string作为缓冲区是不好的做法:

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

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

答案 1 :(得分:64)

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

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

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

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

答案 2 :(得分:9)

  

Memcpying到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()成为非常量指针。

答案 3 :(得分:7)

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

草稿n4659在[string.accessors]中声明:

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

答案 4 :(得分:7)

考虑到这一点,代码是不必要的

firstname surname  
--------- -------   
     ∞    One

将完全相同。

答案 5 :(得分:5)

我将在这里研究的最大优化机会是:Receiver似乎是一种支持.data().size()的容器。如果可以使用它,并将其作为右值引用Receiver&&传递,则可以使用move语义,而无需进行任何复制!如果有迭代器接口,则可以将其用于基于范围的构造函数,也可以将其用于std::move()中的<algorithm>

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

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

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

答案 6 :(得分:4)

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

答案 7 :(得分:0)

我确实认为std::string是用于管理“缓冲区”的合法竞争者;是否是最佳选择取决于几件事...

您的缓冲区内容本质上是文本还是二进制?

您决定的一个主要输入应该是缓冲区内容是否本质上是 textual 。如果std::string用于文本内容,那么对您的代码阅读者来说,混乱的可能性就较小。

char不是用于存储字节的好类型。请记住,C ++标准将其留给每个实现来决定char是带符号的还是不带符号的,但是对于二进制数据的通用黑盒处理(有时甚至将字符传递给诸如std::toupper(int)之类的具有未定义行为的函数时,除非参数在unsigned char的范围内或等于{{1} })您可能想要无符号数据:为什么您假设或暗示每个字节的第一位是不透明的二进制数据是符号位?

因此,不可否认,使用EOF来处理“二进制”数据是有点有点黑。您可以使用std::string,但这不是问题所要解决的问题,使用无处不在的std::basic_string<std::byte>类型会失去一些不可操作性的好处。

使用std :: string的一些潜在好处

首先有一些好处:

  • 它采用了我们都知道并喜欢的RAII语义

  • 大多数实现都具有短字符串优化(SSO),它可以确保如果字节数足够小以直接适合字符串对象内部,则可以避免动态分配/重新分配(但可能会有额外的开销)每次访问数据时分支)

    • 这对于传递读取或要写入的数据副本比较有用,而不是用于缓冲区(缓冲区应预先设置大小以接受适当的数据块,如果有的话)(通过处理更多的I / O来提高吞吐量)一次)
  • 有大量的std::string成员函数和旨在与std::string(例如std::string)配合使用的非成员函数:如果您的客户端代码可以找到它们对解析/操作/处理缓冲区内容很有用,那么您就可以开始了

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

祝福

  • 作为一种熟悉的,无处不在的类型,您与之交互的代码可能具有cout << my_string的专业化名称,更适合您对缓冲数据的使用,或者这些专业性可能更糟:请评估那

有关

正如Waxrat所观察到的那样,缺少API明智的方法是有效地增加缓冲区的能力,因为std::string将NULs /'\ 0写入所添加的字符中,如果您要“接收”值则毫无意义。进入那个记忆。这与正在制作接收数据副本且大小已知的OPs代码无关。

讨论

解决einpoklum的担忧:

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

虽然确实没有std::string最初用于此目的,但其余主要是FUD。该标准对C ++ 17的非std::string成员函数const的这种用法作出了让步,并且char* data()始终支持嵌入的零字节。大多数高级程序员都知道什么是安全的。

替代方法

  • 大小为某个最大消息大小的静态缓冲区(C string数组或char[N]),或在每次调用时传递数据切片

  • 一个带有std::array<char, N>的手动分配的缓冲区,可以自动销毁:您可以随意调整大小,并自己跟踪分配的大小与使用中的大小;总体上更容易出错

  • std::unique_ptr(对于元素类型,可能为std::vector;被广泛理解为暗含二进制数据,但是API的限制性更强,(无论好坏)预期会具有与短字符串优化等效的任何功能。

  • Boost的std::byte:也许,如果SSO是唯一阻止您离开small_vector的东西,并且您对使用boost感到高兴。

  • 返回一个函子,该函子允许延迟访问所接收的数据(前提是您知道它不会被释放或覆盖),从而推迟了由客户端代码存储数据的方式