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


72

我最近看到我的一位同事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;

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


29
在C ++ 17中,data()有一个重载,它返回指向非const限定char的指针。
user7860670

38
...由cppreference提及。这不是cplusplus.com的最佳来源。
HolyBlackCat

11
我认为,从const char*到强制转换的操作char*本身就意味着程序中存在某种危险。如果dst_ptr指向只读存储块,则不应尝试使用此指针写入该存储块。
goodvibration

7
每当您看到使用C样式强制转换的代码(例如(char*) dst_ptr)时,都应将其视为危险信号。
一些程序员花花公子

9
我认为这个问题主要基于意见。std::string如果您知道要接收文本数据,则将IMO用作缓冲区很好。如果您正在接收二进制数据,std::vector<char>则可能是一个更好的选择。
Miles Budnek '19

Answers:


74

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

std::string出于种种原因(不按特定顺序列出),将其用作缓冲区是不好的做法:

  • std::string不打算用作缓冲区;您将需要仔细检查类的描述,以确保没有“陷阱”来防止某些使用模式(或使它们触发未定义的行为)。
  • 举一个具体的例子:在C ++ 17之前,您甚至无法编写通过data()它获得的指针-它是const Tchar *; 因此您的代码将导致未定义的行为。(但是&(str[0])&(str.front())&(*(str.begin()))会起作用。)
  • std::string对缓冲区使用s会使函数定义的读者感到困惑,后者假定您将对std::string字符串使用。换句话说,这样做违反了“最小惊讶原则”
  • 更糟糕的是,这对任何可能使用您的函数的人来说都是令人困惑的-他们也可能认为您返回的内容是字符串,即有效的人类可读文本。
  • std::unique_ptr对于您的情况,甚至还可以std::vector。在C ++ 17中,您也可以将其std::byte用于元素类型。一个更复杂的选项是具有SSO类功能的类,例如Boost的类small_vector(感谢@ gast128,请提及它)。
  • (要点:)libstdc ++必须更改其ABIstd::string才能符合C ++ 11标准,因此在某些情况下(目前不太可能),您可能会遇到一些本来不会存在的链接或运行时问题缓冲区的其他类型。

另外,您的代码可能会分配两个而不是一个堆分配(取决于实现):一次是在构造字符串时,另一次是在执行resize()ing。但这本身并不是真正要避免的原因std::string,因为您可以使用@ Jarod42的答案中的构造来避免重复分配。


3
您能在第一个要点中解释您的意思吗?什么是“陷阱”?
MM

2
使用std::string_view,您可以定义缓冲区的可读部分。
Zereges

16
@MM例如保证出现的终止\0字符std::string。您能立即知道它是否包含在的有效范围内data()吗?覆盖它是UB!它需要一些额外的心理周期(可能还包括参考查找)来验证该功能没有UB。
Max Langhof

3
@MM:重点是-您不直观地知道!我什至不知道MaxLanghof在他的评论中写了什么。
einpoklum

5
为+1 std::byte,这真是难以置信,我之前从未听说过!令人疯狂的是,每天人们经常查看一些C ++参考并且仍然有新事物出现……
andreee

66

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

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

甚至可以处理\0字符串。

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


4
由于原始缓冲区数据不应为const,因此您不应具有UB。
Jarod42 '19

14
然后,一个好的程序员说:“为什么我还需要一个单独的单线转换函数?不仅如此,这个名为“ receive”的函数实际上并不执行任何“ receiving”。删除!”
螺丝螺母

6
@screwnut:公平地说,从理论上讲,有可能receiver.data()等待接收发生,或者做其他事情而不是返回成员指针。
einpoklum

9
@screwnut:那我是一个不好的程序员。我将转换保留在单独的函数中,即使它是一个线性函数也是如此,因为我欣赏抽象并且不喜欢重复自己。如果以后需要添加一些检查,记录等等,该功能就在这里,我不必在代码库中搜寻所有的转换实例。
Matthieu M.19年

5
@screwnut:首选非成员非朋友功能。
Matthieu M.19年

10

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在append上更快的容器,并且您不能std::vector从函数返回。(或者您实际上在C ++ 98中不能这样做,因为C ++ 98需要在函数中构造向量并将其复制出来)。此外,还std::string允许您使用各种成员函数(例如find_first_of和)进行搜索find_first_not_of。当搜索字节数组时,这很方便。

我认为您真正想要/需要的是SGI的Rope类,但它从未进入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()非常量指针。


7
更喜欢std::copy类型安全。不会慢。
Lightness Races in Orbit

3
似乎是直接回答问题的唯一答案。
Keith,

2
“您不能std::vector从函数返回。(或者您不能在C ++ 98或C ++ 03中返回)”是错误的。
Ben Voigt

3
对于编译器来说,将地址与存储的内容混淆是从来没有合法的优化。 buff.data()不能是包含的寄存器'A',它必须是地址。
Ben Voigt

2
@jww:的确,尽管您可以通过NRVO和swap()电话避免使用该副本。但也需要提供副本std::string。小字符串优化可以使其更好一点。我认为某些实现尝试使用写时复制(for string,从未允许vector)解决此问题,但是即使在C ++ 98和C ++ 03中,也存在一些std::stringCOW无法合理满足的规范。当然,右值引用和移动可以很好地解决它。
Ben Voigt

7

从C ++ 17,data可以返回一个非const char *

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

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

8
@SergeBallesta-删除const限定词不是UB。修改const对象是UB。有问题的对象不是const。
StoryTeller-Unslander Monica,

6
@SergeBallesta-认真吗?以及如何&str[0]使它成为指向同一缓冲区的非常量指针呢?保证该对象不是const。该语言的核心规则仍然适用,甚至适用于从库类型ergo(无UB)返回的指针。
StoryTeller-Unslander Monica

4
@ Jarod42:我同意我在这里挑剔,但是该库可能期望不更改缓冲区,然后再使用缓存的版本。现在,由于优化编译器而对旧的K&R CI感到恐惧,并且对于常量性和严格的别名非常谨慎。
Serge Ballesta

8
@StoryTeller Serge是正确的,“修改通过数据的const重载访问的字符数组具有未定义的行为。” 根据cppreference标准
Max Langhof


7

考虑到

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

会做完全一样的。


3
您可以削减更多代码;这if也是不必要的。assign那将是无人值守。但是,继续执行不必要的删除代码,最终得到Jarod42的答案。没有这些线是必要的,因为std::string已经有一个合适的构造函数。
MSalters

@MSalters我不想假设没有给出的东西。如果receiver.size()可以返回负值怎么办?
套件。

鉴于大小通常为a size_t,因此是无符号的,这将是非常意外的。这确实表明您的代码可能存在问题:它可能遭受带符号的整数溢出,这是未定义的行为。而且它位于处理输入的代码路径上,因此这可能构成一个可从外部利用的漏洞。
MSalters

@MSalters是的,Jarod42的更改可能会引入一个外部可利用的漏洞。如果receiver.data()UBreceiver.size()为零,它们也可能导致崩溃。
套件。

5

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

在C ++ 17中(如Serge Ballesta等人提到的那样),std::string::data()返回指向非常量数据的指针。std::string已经保证A连续存储所有数据。

尽管不是真正的程序员的错,但是书面的代码有点散发出来的气味:这些黑客在当时是必需的。今天,你至少应该改变的类型,dst_ptrconst char*char*并删除投中的第一个参数memcpy()。您还可以为reserve()缓冲区添加多个字节,然后使用STL函数移动数据。

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


4

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


0

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

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

决定的一个主要输入应该是缓冲区内容是否本质上是文本的。如果std::string用于文本内容,那么它对代码阅读者的潜在混乱将较小。

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

因此,毫无疑问std::string用于“二进制”数据有点。您可以使用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()将NUL /'\ 0写入所添加的字符中,如果您要“接收”到该内存中的值是没有意义的。这与正在制作接收数据副本且大小已知的OPs代码无关。

讨论区

解决einpoklum的问题:

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

确实std::string不是原先打算这样做的,但其余的主要是FUD。该标准通过C ++ 17的非const成员函数对这种用法做出了让步char* data(),并且string始终支持嵌入的零字节。大多数高级程序员都知道什么是安全的。

备择方案

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

  • 手动分配的缓冲区,std::unique_ptr用于自动销毁:让您轻松调整大小,并自己跟踪分配的和使用中的大小;总体上更容易出错

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

  • Boost的small_vector:也许,如果SSO是唯一使std::vector您无法前进的东西,并且您对使用boost很高兴。

  • 返回一个函子,该函子允许延迟访问所接收的数据(前提是您知道不会对其进行重新分配或覆盖),从而推迟了由客户端代码存储数据的方式的选择

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.