为什么C ++ 11使std :: string :: data()添加一个空终止符?

han*_*rik 29 c++ string stdstring c++11 c++03

以前这是std::string::c_str()工作,但是从C ++ 11开始,data()它也提供了工作,为什么还要c_str()添加null终止字符std::string::data()?在我看来,这似乎是在浪费CPU周期,如果null终止字符根本不相关而仅data()使用,C ++ 03编译器就不必关心终止符,也不必每次调整字符串大小时,都必须向终止符写入0,但是由于data()-null-guarantee,C ++ 11编译器不得不浪费每次写入字符串大小时都要写入0的周期,因此,这可能会使代码变慢,我想他们有一定理由要添加该保证,那是什么?

Aco*_*orn 28

这里有两点要讨论:

空终止符的空间

理论上,C ++ 03的实现可能已经避免了终止分配空间和/或可能需要执行拷贝(如不共享)。

但是,所有理智的实现都为null终止符分配了空间以便支持c_str()开始,因为否则,如果这不是一个琐碎的调用,则实际上将无法使用。

空终止符本身

确实,某些非常(1999),非常老的实现(2001)编写了\0每一个c_str()调用。

但是,为了避免在C ++ 11发布之前采用这种方式,主要的实现方式发生了变化(2004)或已经很像那样(2010),因此,当新标准出现时,对于许多用户而言,它们没有任何变化。

现在,无论C ++ 03实现是否应该这样做:

在我看来,这似乎浪费了CPU周期

并不是的。如果调用c_str()不止一次,则已经多次写入已在浪费周期。不仅如此,您还使高速缓存层次结构混乱,在多线程系统中考虑这一点很重要。回想一下,多核/ SMT CPU在2001年2006年之间开始出现,这解释了向现代非CoW实施的转换(即使在此之前的几十年中就已经有了多CPU系统)。

唯一会保存任何内容的情况是,如果您从未致电过c_str()。但是,请注意,在重新调整字符串大小时,无论如何都在重写所有内容。额外的字节将几乎无法测量。

换句话说,通过将终止符写成调整大小,您会使自己暴露于更差的性能/延迟中。通过同时写入一次,您必须执行字符串的副本,性能行为将更加可预测,并且,如果最终使用c_str(),尤其在多线程系统上,可以避免性能下降。

  • “此代码可能会运行得更快”->需要测量 (4认同)
  • @eerorika是的,关键是人们已经意识到C ++ 11之前的现代实现方式(即非CoW等)。 (2认同)

eer*_*ika 25

变更的优点:

  1. data也保证了空终止,程序员不需要知道之间的差异模糊的细节c_str,并data因此将避免传递字符串不空终止的保证成空要求终止函数未定义行为。此类函数在C接口中无处不在,并且C接口在C ++中经常使用。

  2. 下标运算符也已更改为允许对进行读取访问str[str.size()]。不允许访问str.data() + str.size()将是不一致的。

  3. 虽然在调整大小等操作时未初始化空终止符可能会使该操作更快,但会强制进行初始化,c_str从而使该功能变慢¹。删除的优化案例并不是普遍更好的选择。考虑到第2点中提到的更改,缓慢性也会影响下标运算符,这肯定对性能不可接受。这样,无论如何,空终止符都会存在,因此保证它不会有不利影响。

好奇的细节:str.at(str.size())仍然引发异常。

PS还有一个更改,就是要确保字符串具有连续的存储(这就是为什么data首先提供字符串的原因)。在C ++ 11之前,实现可能使用绳索字符串,并在调用时重新分配c_str。(据我所知)没有任何主要的实现方案选择利用这种自由。

PPS例如,GCC的libstdc ++的旧版本显然仅将空终止符设置为c_str3.4版。有关详细信息,请参见相关的提交


¹导致此问题的一个因素是C ++ 11中的语言标准引入了并发性。并发的非原子性修改是数据争用未定义的行为,这就是为什么允许C ++编译器进行积极的优化并将其保存在寄存器中的原因。因此,用普通C ++编写的库实现将具有UB用于并发调用.c_str()

在实践中(请参阅注释),因为有多个线程编写相同的内容,所以不会引起正确性问题,因为实际CPU的asm没有UB。C ++ UB规则意味着,多个线程实际上在不同步的情况下修改了一个std::string对象(而不是调用c_str()),这是编译器+库可以假定不会发生的事情。

但是它会弄脏缓存并阻止其他线程读取它,因此仍然是一个糟糕的选择,尤其是对于可能具有并发读取器的字符串。.c_str()由于商店的副作用,它也不会从根本上优化。


Dav*_*aim 13

问题的前提是有问题的。

字符串类必须做很多事情,例如分配动态内存,将字节从一个缓冲区复制到另一个缓冲区,释放基础内存等等。

一件糟糕的mov组装说明让您烦恼吗?相信我,这不会对您的表现造成0.5%的影响。

在编写编程语言运行时时,您不必沉迷于每条小的汇编指令。您必须明智地选择优化方案,而优化不引人注意的空终止并不是其中之一。

在这种特定情况下,与C兼容比空终止更为重要。

  • @Yksisarvinen“您不用为不需要的东西付钱”,这一切都不会动摇。为什么`std :: future`必须是线程安全的?我想将其用于我的单线程应用程序。使它成为非线程安全的。我可以直接进入C ++标准的任何类,找到我的应用程序使用的**比提供的少**的情况,并抱怨我为不使用的东西付费 (8认同)
  • @DavidHaim`std :: shared_ptr`是向`std`添加线程安全成本的更好示例。 (3认同)
  • 虽然我同意有关微优化的说法,但C ++通常遵循“您不需要为所需的东西付费”的规则。而且由于我们已经具有与C兼容的c_str(),所以我看不出使data()与c_str()的同义词有什么帮助。 (2认同)
  • C ++的口号是尽可能与C兼容(至少在ABI和呼叫常规级别上) (2认同)