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

duo*_*gja 68 c++ stdstring c++11

我最近看到我的一位同事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;
}
Run Code Online (Sandbox Code Playgroud)

我猜这家伙想利用返回字符串的自动销毁功能,因此他不必担心释放分配的缓冲区。

这对我来说有点奇怪,因为根据cplusplus.com,data()方法返回const char*指向由字符串内部管理的缓冲区的指针:

const char* data() const noexcept;
Run Code Online (Sandbox Code Playgroud)

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

ein*_*ica 70

不要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 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 (thank you, @gast128, for mentioning it).
  • (Minor point:) libstdc++ had to change its ABI for std::string to conform to the C++11 standard, which in some cases (which by now are rather unlikely), you might run into some linkage or runtime issues that you wouldn't with a different type for your buffer.

Also, your code may make two instead of one heap allocations (implementation dependent): Once upon string construction and another when resize()ing. But that in itself is not really a reason to avoid std::string, since you can avoid the double allocation using the construction in @Jarod42's answer.

  • @MM例如,终止字符“ \ 0”,其中“ std :: string”保证存在。您能立即知道它是否包含在`data()`的有效范围内吗?[覆盖它是UB](https://en.cppreference.com/w/cpp/string/basic_string/data)!它需要一些额外的心理周期(可能还包括参考查找)来验证该功能没有UB。 (16认同)
  • +1为`std :: byte`,令人难以置信,我之前从未听说过!令人疯狂的是,每天人们经常查看一些C ++参考,而新事物还在不断涌现... (5认同)
  • 您能在第一个要点中解释您的意思吗?什么是“陷阱”? (3认同)
  • @MM:重点是-您不直观地知道!我什至不知道MaxLanghof在他的评论中写了什么。 (3认同)
  • 使用`std :: string_view`,您可以*定义*缓冲区的可读部分。 (2认同)

Jar*_*d42 64

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

std::string receive_data(const Receiver& receiver) {
    return {receiver.data(), receiver.size()};
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 然后,一个好的程序员说:“为什么我还需要一个单独的单线转换函数?不仅如此,这个名为“ receive”的函数实际上并不执行任何“ receiving”。删除!” (14认同)
  • @screwnut:那我是一个不好的程序员。我将转换保留在单独的函数中,即使它是一个线性函数也是如此,因为我欣赏抽象并且不喜欢重复自己。如果以后需要添加一些检查,日志记录等...该功能就在这里,我不必在代码库中寻找所有转换实例。 (9认同)
  • @screwnut:公平地说,从理论上讲,`receiver.data()`有可能等待接收发生,或者除了返回成员指针外还做其他事情。 (6认同)
  • @screwnut:首选非成员非朋友功能。 (5认同)
  • 由于原始缓冲区数据不应为“ const”,因此您不应具有UB。 (4认同)

jww*_*jww 9

Memcpying到一个const char指针?AFAIK只要知道我们所做的事情就不会造成任何伤害,但这是好的行为,为什么?

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

buff.resize(size);
memcpy(&buff[0], &receiver[0], size);
Run Code Online (Sandbox Code Playgroud)

我最近看到我的一位同事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);
Run Code Online (Sandbox Code Playgroud)

我知道在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);
Run Code Online (Sandbox Code Playgroud)

GCC将单个字节'A'放入寄存器中AL。高3个字节是垃圾,因此32位寄存器是0xXXXXXX41。当我取消引用时ptr[0],GCC取消引用了垃圾地址0xXXXXXX41

对我来说,两个要点是,不要编写半屁股的自我测试,也不要尝试制作data()非常量指针。

  • 对于类型安全性,最好使用“ std :: copy”。不会慢。 (7认同)
  • 似乎是直接回答问题的唯一答案。 (3认同)
  • 对于编译器来说,将地址与存储的内容混淆是从来没有合法的优化。buff.data()不能是包含“ A”的寄存器,它必须是地址。 (3认同)
  • “您不能从函数中返回`std :: vector`。(或者您不能在C ++ 98或C ++ 03中返回)”是错误的。 (2认同)
  • @jww:确实,尽管您可以使用NRVO和一个`swap()`调用来避免该副本。但是副本也需要`std :: string`。小字符串优化可以使其更好一点。我认为一些实现尝试使用写时复制来解决此问题(对于“字符串”,从未允许使用“向量”),但是即使在C ++ 98和C ++ 03中,也有一些关于“ std :: string”的规范COW无法合理满足。当然右值引用和移动可以很好地解决它。 (2认同)

Ser*_*sta 7

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

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

const charT* c_str() const noexcept;
const charT* data() const noexcept;
....
charT* data() noexcept;
Run Code Online (Sandbox Code Playgroud)

  • @SergeBallesta-删除常量限定符不是** UB。修改const对象是UB。有问题的对象不是const。 (8认同)
  • @StoryTeller Serge是正确的,_“修改通过数据的const重载访问的字符数组具有未定义的行为。” _根据[cppreference](https://en.cppreference.com/w/cpp/string/basic_string/data )和[标准](http://eel.is/c++draft/basic.string#string.accessors-3)。 (8认同)
  • @SergeBallesta-认真吗?以及如何将`&str [0]`用作指向同一缓冲区的非常量指针来调和呢?保证该对象不是const。该语言的核心规则仍然适用,甚至适用于从库类型ergo(无UB)返回的指针。 (6认同)
  • @ Jarod42:我同意我在这里挑剔,但是该库可能期望不更改缓冲区,然后再使用缓存的版本。现在,由于优化编译器而对旧的K&R CI感到恐惧,并且对常量性和严格的别名非常谨慎。 (4认同)
  • (为了完整起见,这是C ++ 11的措辞:https://timsong-cpp.github.io/cppwp/n3337/string.ops#string.accessors-3) (4认同)
  • @SergeBallesta-仅当它实际上指向带有const限定符的“声明”对象时。不会的,因为`operator []`可以在相同的缓冲区中返回对char的非常量引用(从C ++ 11开始保证)。 (3认同)
  • 仍然是注释,因为与原始问题无关,但草案n3337在21.4.7.1 basic_string访问器[string.accessors]中有一个要求,它声明c_str和data:§3* 3要求:程序不得更改以下任何内容:存储在字符数组中的值。*。 (3认同)
  • @duong_dajgja:在C ++ 17`data`仅返回`const`指针之前,因此更改其内容正式为Undefined Behaviour。 (2认同)
  • @StoryTeller:指针由标准库中的方法返回,并声明为const。的确,在所有常见的实现中,对象不是const,而是根据标准,并且在C ++ 17之前,不允许删除限定符。 (2认同)
  • @SergeBallesta-你是对的。我曾经进行过段错误处理,因为我使用了data()并丢弃了const。在自检套件中,该字符串是1个字符的测试向量,并且GCC对其进行了优化并将其放入寄存器中,而不是使其成为字节数组。自检的功能类似于`char * ptr = s.data();。ptr [0] ^ = 1;`篡改一个字节,以查看HMAC是否会检测到篡改。 (2认同)

Kit*_*it. 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;
}
Run Code Online (Sandbox Code Playgroud)

将做完全一样。

  • 您可以削减更多代码;如果不是也没有必要。那么`assign`将是无操作的。但是继续进行不必要的删除代码,最终得到Jarod42的答案。这些行的_None_是必需的,因为`std :: string`已经具有适当的构造函数。 (2认同)

Dav*_*lor 5

我将在这里研究的最大优化机会是:Receiver似乎是某种支持.data()和的容器.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将是更自然的数据结构。