C ++ 11中COW std :: string实现的合法性


117

据我了解,写时复制不是std::string在C ++ 11中实现符合性的可行方法,但是最近在讨论中出现时,我发现自己无法直接支持该声明。

我是否正确C ++ 11不允许基于COW的实现std::string

如果是这样,此限制是否在新标准的某处(何处)明确说明?

或暗示此限制,是因为新要求的综合影响std::string使不能基于COW的实现std::string。在这种情况下,我会对“ C ++ 11有效禁止基于COW的std::string实现” 一章和诗句样式的衍生感兴趣。


5
他们的COW字符串的GCC错误是gcc.gnu.org/bugzilla/show_bug.cgi?id=21334#c45。跟踪libstdc ++中std :: string的新C ++ 11编译器实现的错误之一是gcc.gnu.org/bugzilla/show_bug.cgi?id=53221
user7610 2014年

Answers:


120

这是不允许的,因为按照标准21.4.1 p6,迭代器/引用的无效仅适用于

—作为任何标准库函数的参数,并以对非const basic_string的引用作为参数。

—在front,back,begin,rbegin,end和rend处调用非const成员函数,但operator []除外。

对于COW字符串,调用非const operator[]将需要进行复制(并使引用无效),而上述段落不允许这样做。因此,在C ++ 11中拥有COW字符串不再合法。


4
基本原理:N2534
MM

8
-1逻辑不成立。在进行COW复制时,没有可以使之无效的引用或迭代器,进行复制的全部要点是,现在正在获取此类引用或迭代器,因此必须进行复制。但是C ++ 11仍然可能不允许COW实现。
干杯和健康。-Alf 2014年

11
@ Cheersandhth.-Alf:如果允许COW,则可以在下面看到逻辑: std::string a("something"); char& c1 = a[0]; std::string b(a); char& c2 = a[1]; c1是对a的引用。然后,您“复制”一个。然后,当您第二次尝试获取引用时,它必须进行复制以获得非常量引用,因为有两个字符串指向同一缓冲区。这将使第一个引用无效,并且与上面引用的部分相反。
Dave S

9
@ Cheersandhth. -阿尔夫,根据这个,至少GCC的COW的实现并不做什么DAVES在说什么。因此,至少标准禁止这种COW样式。
塔维安·巴恩斯

4
@Alf:这个答案认为非const operator[](1)必须复制,而(2)这样做是非法的。您不同意这两个观点中的哪一个?查看您的第一条评论,看来一个实现可以至少在此要求之下共享字符串,直到访问该字符串为止,但是读写访问都需要取消共享它。那是你的意思吗?
Ben Voigt 2015年

48

通过这些问题的答案戴夫小号gbjbaanb正确的。(而且Luc Danton的观点也是正确的,尽管它是禁止COW字符串的副作用,而不是禁止它的原始规则。)

但是为了澄清一些混乱,我将添加一些进一步的说明。各种注释链接到我对GCC bugzilla的注释,其中给出了以下示例:

std::string s("str");
const char* p = s.data();
{
    std::string s2(s);
    (void) s[0];
}
std::cout << *p << '\n';  // p is dangling

该示例的目的是演示为什么GCC的引用计数(COW)字符串在C ++ 11中无效。C ++ 11标准要求此代码才能正常工作。代码中的p任何内容都不允许在C ++ 11中使无效。

使用GCC的旧的引用计数std::string实现,该代码具有无效的行为,因为p 无效了,变成了悬空的指针。(发生的事情是,在s2构造时,它与共享数据s,但通过获取非常量引用s[0]需要取消共享数据,s“写时复制”也是如此,因为引用s[0]有可能被用于写入s,然后s2转到超出范围,破坏p)指向的数组。

C ++ 03标准在21.3 [lib.basic.string] p5中明确允许该行为,它表示在调用data()第一个调用之后operator[]()可能会使指针,引用和迭代器无效。因此,GCC的COW字符串是有效的C ++ 03实现。

C ++ 11标准不再允许该行为,因为对的调用不会operator[]()使指针,引用或迭代器无效,无论它们是否跟随对的调用data()

因此,上面的示例必须在C ++ 11 工作,但不适用于libstdc ++的那种COW字符串,因此,在C ++ 11中不允许这种类型的COW字符串。


3
在调用.data()(以及每次返回指针,引用或迭代器)上取消共享的实现不会遇到该问题。即(不变)缓冲区在任何时候都是不共享的,或者没有外部引用共享。我以为您打算将此示例的评论作为非正式的错误报告作为注释,非常抱歉,误解了!但是正如您通过考虑我在此描述的实现所看到的那样,当noexcept忽略需求时,该实现在C ++ 11中可以正常工作,该示例并未说明任何形式上的要求。如果您愿意,我可以提供代码。
干杯和健康。-Alf 2015年

7
如果取消共享几乎所有对字符串的访问,那么您将失去共享的所有好处。对于标准库来说,COW实现必须实用,以便将它用作as std::string,并且我真诚地怀疑,您可以演示满足C ++ 11无效要求的有用,高性能的COW字符串。因此,我认为noexcept最后一刻添加的规范是禁止COW字符串的结果,而不是根本原因。N2668似乎非常清楚,为什么您继续否认那里概述的委员会意图的明确证据?
Jonathan Wakely 2015年

另外,请记住,这data()是一个const成员函数,因此必须安全地与其他const成员data()并发调用,例如,与另一个复制该字符串的线程并发调用。因此,对于每个字符串操作,甚至是const操作,您都将需要互斥锁的所有开销,或者无锁的可变引用计数结构的复杂性,并且毕竟,只有在您从未修改或访问时,您才需要共享您的字符串如此之多,许多字符串的引用计数为1。请提供代码,随时忽略noexcept保证。
Jonathan Wakely 2015年

2
现在将一些代码整理在一起,我发现有129个basic_string成员函数以及免费函数。抽象成本:对于g ++和MSVC,这种现成的未经优化的最新零版本代码要慢50%至100%。它不具有线程安全性(shared_ptr我想很容易利用),它仅足以支持为定时目的对字典进行排序,但由于模块错误,证明了basic_string除C ++ noexcept要求之外,允许引用计数的观点。 github.com/alfps/In-principle-demo-of-ref-counted-basic_string
干杯和hth。-阿尔夫


20

是的,CoW是制作更快字符串的可接受机制...但是...

这会使多线程代码变慢(所有使用锁定来检查您是否是唯一的写作都会在使用大量字符串时降低性能)。这是CoW多年前被杀死的主要原因。

另一个原因是[]操作员将返回您的字符串数据,而没有任何保护您覆盖其他人希望不变的字符串的保护。c_str()和和相同data()

快速谷歌表示,多线程基本上是有效禁止(未明确)的原因。

提案说:

提案

我们建议使所有迭代器和元素访问操作安全地并发执行。

即使在顺序代码中,我们也在提高操作的稳定性。

此更改有效地禁止了写时复制实现。

其次是

由于转向写时复制实现而导致的性能潜在的最大损失是,对于具有很大的只读字符串的应用程序,内存消耗增加了。但是,我们认为对于这些应用程序,绳索是一种更好的技术解决方案,因此建议考虑将绳索建议包括在库TR2中。

绳索是STLPort和SGI STL的一部分。


2
操作员[]问题并不是真正的问题。const变体确实提供了保护,非const变体始终可以选择在那时执行CoW(或非常疯狂并设置页面错误来触发它)。
Christopher Smith

+1转到问题。
干杯和健康。-Alf 2014年

5
愚蠢的是,不包含std :: cow_string类,lock_buffer()等。很多时候我都知道线程化不是问题。实际上,很多时候。
Erik Aronesty

我喜欢ig绳索的替代建议。我想知道是否还有其他替代类型和实现。
伏尔泰

5

从21.4.2 basic_string构造函数和赋值运算符[string.cons]

basic_string(const basic_string<charT,traits,Allocator>& str);

[...]

2 效果basic_string如表64所示构造一个类的对象。[...]

表64有用地记录了通过此(复制)构造函数构造对象后的this->data()值:

指向数组的已分配副本的第一个元素,该数组的第一个元素由str.data()指向

其他类似的构造函数也有类似的要求。


+1说明C ++ 11(至少部分)如何禁止COW。
干杯和健康。-Alf 2014年

对不起,我累了。除了当前共享缓冲区之外,对.data()的调用必须触发COW复制的解释不多。仍然是有用的信息,所以我让upvote站起来。
干杯和健康。-Alf 2014年

1

由于现在可以保证字符串是连续存储的,并且现在允许您使用指向字符串内部存储的指针(即,&str [0]的工作方式类似于数组),因此无法创建有用的COW实施。您将不得不为太多事情制作一份副本。即使只是在非const字符串上使用operator[]begin()在其上也需要复制。


1
我认为C ++ 11中的字符串可以保证连续存储。
mfontanini 2012年

4
在过去,你不得不做的副本在所有这些情况下,这是没有问题的...
dribeas大卫-罗德里格斯

@mfontanini是的,但以前不是这样
Dirk Holsopple 2012年

3
尽管C ++ 11确实保证字符串是连续的,但这与禁止COW字符串正交。GCC的COW字符串是连续的,因此很明显您的主张“不可能实现有用的COW实现”是虚假的。
Jonathan Wakely 2015年

1
@supercat,要求后备存储(例如,通过调用c_str())必须是O(1)并且不能抛出,并且必须不引入数据争用,因此,如果您延迟连接,很难满足这些要求。实际上,唯一合理的选择是始终存储连续数据。
Jonathan Wakely 2015年

1

basic_string在C ++ 11及更高版本中是否禁止COW ?

关于

我是正确的是C ++ 11不承认基于COW的实现std::string

是。

关于

如果是,此限制是否在新标准的某个地方(where)中明确说明?

几乎直接地,对于许多操作的恒定复杂性的要求,这将需要在COW实现中对字符串数据进行O( n)物理复制。

例如,对于成员函数

auto operator[](size_type pos) const -> const_reference;
auto operator[](size_type pos) -> reference;

…在COW实现中,这将“触发字符串数据复制以取消共享字符串值”,C ++ 11标准要求

C ++ 11§21.4.5/ 4

复杂度:常量时间。

……排除了此类数据复制,因此也禁止了COW。

C ++ 03通过支持COW实现具有这些恒定的复杂性的要求,并且,通过在一定的限制条件,从而允许呼叫operator[]()at()begin()rbegin()end(),或rend()到无效引用指针和迭代参照串项目,即,以可能招致COW数据复制。此支持已在C ++ 11中删除。


是否通过C ++ 11无效规则也禁止COW?

在写本文时被选为解决方案的另一个答案中,该答案被强烈推崇,因此显然被认为是:

对于一个COW字符串,在调用非const operator[]需要进行复印(和无效的参考文献),它是由[引述]段落[C ++ 11§21.4.1/ 6]上述禁止。因此,在C ++ 11中拥有COW字符串不再合法。

该断言是不正确的,并且在两个主要方面具有误导性:

  • 它错误地指示只有非const项目访问者才需要触发COW数据复制。
    但是const项目访问器也需要触发数据复制,因为它们允许客户端代码形成引用或指针(在C ++ 11中),以后不允许通过触发COW数据复制的操作使它们无效。
  • 它错误地假定COW数据复制会导致引用无效。
    但是,在正确的实现方式中,取消共享字符串值的COW数据复制是在任何引用无效之前进行的。

要了解正确的C ++ 11 COW实现如何basic_string工作,当使该无效的O(1)要求被忽略时,请考虑一个可以在所有权策略之间切换字符串的实现。字符串实例以策略Sharable开始。启用此策略后,将不会有任何外部项目引用。实例可以转换为唯一策略,并且在可能创建项目引用(例如调用)时.c_str()(至少在生成指向内部缓冲区的指针的情况下),它必须这样做。在多个实例共享值所有权的一般情况下,这需要复制字符串数据。在过渡到“唯一”策略之后,实例只能通过使所有引用(例如分配)无效的操作过渡回“可共享”状态。

因此,尽管该答案得出的结论是排除了COW字符串,但它是正确的,但所提供的推理是不正确的,并且极易引起误解。

我怀疑造成这种误解的原因是C ++ 11的附件C中的非规范性注释:

C ++ 11§C.2.11[diff.cpp03.strings],关于§21.3:

更改basic_string需求不再允许引用计数的字符串
依据:无效与引用计数的字符串有所不同。此更改使该国际标准的行为规范化。
对原始功能的影响:有效的C ++ 2003代码在本国际标准中的执行方式可能有所不同

这里的基本原理解释了为什么决定删除C ++ 03特殊COW支持的主要原因。该理由(为什么)不是该标准如何有效地禁止COW实施。该标准不允许通过O(1)要求进行COW。

简而言之,C ++ 11无效规则不排除的COW实现std::basic_string。但是他们确实排除了合理有效的不受限制的C ++ 03样式的COW实现,例如至少在g ++的一种标准库实现中。特殊的C ++ 03 COW支持const以较低的,复杂的无效规则为代价,从而提高了实用效率,尤其是使用项目访问器时:

C ++ 03§21.3/ 5,其中包括“首次致电” COW支持:

引用basic_string序列元素的引用,指针和迭代器可能 由于该basic_string对象的以下用法而无效:
—作为非成员函数swap()(21.3.7.8),operator>>()(21.3.7.9)和getline()(21.3)的参数。 7.9)。
—作为的参数basic_string::swap()
—调用data()c_str()成员函数。
-调用非const成员函数,除了operator[]()at()begin()rbegin()end(),和rend()
-之后任何上述用途以外的形式insert()erase()其返回迭代器,所述第一呼叫到非const成员函数operator[]()at()begin()rbegin()end()rend()

这些规则是如此复杂和微妙,以至于我怀疑许多程序员(如果有的话)能否给出准确的总结。我不能。


如果忽略O(1)要求怎么办?

如果operator[]忽略例如C ++ 11的恒定时间要求,则COW for basic_string在技​​术上可能可行,但难以实现。

无需访问COW数据即可访问字符串内容的操作包括:

  • 通过串联+
  • 通过输出<<
  • basic_string对标准库函数使用as参数。

后者是因为允许标准库依赖于实现特定的知识和构造。

另外,一个实现可以提供各种非标准功能来访问字符串内容,而无需触发COW数据复制。

一个主要的复杂因素是,在C ++ 11中,basic_string项目访问必须触发数据复制(取消共享字符串数据),但必须不抛出,例如C ++ 11§21.4.5/ 3“ Throws: Nothing。”。因此,它不能使用普通的动态分配为COW数据复制创建新的缓冲区。解决此问题的一种方法是使用特殊的堆,在其中可以保留内存而无需实际分配,然后为每个对字符串值的逻辑引用保留必需的数量。在这样的堆中保留和取消保留可以是恒定时间O(1),而分配一个已经保留的量可以是noexcept。为了符合该标准的要求,使用这种方法似乎每个单独的分配器都需要一个这样的基于特殊保留的堆。


注意:
¹ const项目访问器触发COW数据复制,因为它允许客户端代码获取对数据的引用或指针,例如,非const项目访问器触发的更高版本的数据复制不允许该引用或指针无效。


3
您的示例是一个错误的C ++ 11实现的很好的例子。可能它对于C ++ 03是正确的。” 是的,这就是示例的重点。它显示了在C ++ 03中合法的COW字符串,因为它不违反旧的迭代器失效规则,而在C ++ 11中不合法,因为它确实违反了新的迭代器失效规则。这也与我在上面的评论中引用的声明相矛盾。
Jonathan Wakely

2
如果您说“ 共享” 最初没有被共享,那我就不会争论。说最初是共享的东西只是令人困惑。与自己共享?这个词不是这个意思。但是我重复一遍:您试图证明C ++ 11迭代器无效规则没有禁止某些假设的COW字符串,而这些COW字符串确实确实禁止了那种COW字符串,但这些字符串从未在实践中使用过(并且性能会令人无法接受)。在实践中使用这种方法在学术上是毫无意义的。
Jonathan Wakely

5
你提出的COW字符串很有趣,但我不知道怎么有用的那样。COW字符串的重点是仅在写入两个字符串的情况下才复制字符串数据。建议的实现需要在发生任何用户定义的读取操作时进行复制。即使编译器只知道其读操作,也必须复制。此外,复制唯一字符串将导致其字符串数据的副本(可能是共享状态),这再次使COW变得毫无意义。因此,在没有复杂性保证的情况下,您可以编写……一个非常糟糕的 COW字符串。
Nicol Bolas

2
因此,尽管从技术上讲,复杂性保证可以阻止您编写任何形式的COW,但实际上[basic.string] / 5阻止了您编写任何真正有用的形式的COW字符串。
Nicol Bolas

4
@JonathanWakely:(1)您的报价不是问题。这是一个问题:“我是否纠正C ++ 11不接受基于COW的std :: string实现?如果是这样,此限制是否在新标准的某个地方(哪里)明确说明?” (2)您认为std::string,当您忽略O(1)要求时,COW 会效率低下。我不知道这种表现会是什么样,但我认为提出这种主张更多是因为它的感觉,它所传达的共鸣,而不是与这个答案有任何关联。
干杯和健康。-Alf

0

我一直在想不可变的母牛:一旦母牛被创建,我只能通过从另一头母牛分配而改变,因此它将符合标准。

今天,我有时间尝试进行简单的比较测试:一个大小为N的映射,由字符串/牛作为键,每个节点都持有映射中所有字符串的集合(我们有NxN个对象)。

在字符串大小约为300字节且N = 2000的情况下,母牛的速度稍快一些,并且使用的内存几乎减少了一个数量级。参见下文,大小以kbs为单位,b代表奶牛。

~/icow$ ./tst 2000
preparation a
run
done a: time-delta=6 mem-delta=1563276
preparation b
run
done a: time-delta=3 mem-delta=186384
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.