我最近看到我的一位同事使用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*
,指向由字符串内部管理的缓冲区:< / p>
const char* data() const noexcept;
存储到const char指针? AFAIK只要知道我们所做的事情就不会造成伤害,但是我错过了什么吗?这很危险吗?
答案 0 :(得分:69)
std::string
作为缓冲区。出于某些原因(以不特定的顺序列出),使用std::string
作为缓冲区是不好的做法:
std::string
不能用作缓冲区;您需要仔细检查类的描述,以确保没有“陷阱”阻止某些使用模式(或使它们触发未定义的行为)。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,值得一提)。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_of
和find_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::vector
或std::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>
类型会失去一些不可操作性的好处。
首先有一些好处:
它采用了我们都知道并喜欢的RAII语义
大多数实现都具有短字符串优化(SSO),它可以确保如果字节数足够小以直接适合字符串对象内部,则可以避免动态分配/重新分配(但可能会有额外的开销)每次访问数据时分支)
有大量的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感到高兴。
返回一个函子,该函子允许延迟访问所接收的数据(前提是您知道它不会被释放或覆盖),从而推迟了由客户端代码存储数据的方式