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

Legality of COW std::string implementation in C++11

本文关键字:实现 合法性 string C++11 COW std      更新时间:2023-10-16

我一直认为写时复制不是在C++11中实现一致std::string的可行方法,但当它最近在讨论中出现时,我发现自己无法直接支持这一说法。

C++11不允许基于COW的std::string实现,我说得对吗?

如果是,新标准中是否明确规定了这一限制(哪里)?

或者,这种限制是隐含的,因为它是新要求对std::string的综合影响,排除了基于COW的std::string的实现。在这种情况下,我会对"C++11有效地禁止基于COW的std::string实现"的章节式派生感兴趣。

这是不允许的,因为根据标准21.4.1 p6,只有才允许迭代器/引用无效

作为引用的任何标准库函数的参数到非常数basic_string作为参数。

--调用非常数成员函数,运算符[]除外,at、front、back、begin、,结束,然后撕裂。

对于COW字符串,调用非常量operator[]将需要制作一个副本(并使引用无效),而上面的段落不允许这样做。因此,在C++11中使用COW字符串不再是合法的。

Dave S和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字符串,因此这种COW字符串在C++11中是不允许的。

事实上,CoW是一种可以接受的机制,用于生成更快的字符串。。。但是

它会使多线程代码变得更慢(当使用大量字符串时,所有用于检查是否只有您在写的锁定都会影响性能)。这是几年前CoW被杀的主要原因。

其他原因是[]运算符将向您返回字符串数据,而不会为您覆盖其他人期望不变的字符串提供任何保护。这同样适用于c_str()data()

Quick google表示,多线程基本上是它被有效禁止的原因(没有明确)。

该提案称:

建议书

我们建议安全地进行所有迭代器和元素访问操作同时可执行。

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

此更改实际上不允许写时复制实现。

后面跟着

由于从写时复制实现增加了内存消耗适用于具有非常大的只读字符串的应用程序。然而,我们相信对于这些应用来说,绳索是一种更好的技术解决方案,并建议考虑将绳索方案纳入图书馆TR2。

Ropes是STLPort和SGIs STL的一部分。

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()指向

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

在C++11及更高版本中是否禁止COWbasic_string

关于

C++11不允许std::string的基于COW的实现,我说得对吗?

关于

如果是,新标准中是否明确规定了这一限制(哪里)?

几乎是直接的,因为在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字符串,调用非constoperator[]将需要制作一个副本(并使引用无效),这是[C++11§21.4.1/6]上面[引用]段所不允许的。因此,在C++11中拥有COW字符串不再合法。

该断言在两个主要方面是不正确和误导的:

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

要了解basic_string的正确C++11 COW实现如何工作,当使其无效的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无效规则不排除std::basic_string的COW实现。但他们确实排除了一个相当有效的无限制C++03风格的COW实现,比如至少一个g++的标准库实现中的COW。特殊的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.7)的参数。
-作为basic_string::swap()的参数
--调用data()c_str()成员函数
--调用除operator[]()at()begin()rbegin()end()rend()之外的非const成员函数
--除了返回迭代器的insert()erase()的形式之外,在上述任何使用之后,对非const成员函数operator[]()at()begin()rbegin()end()rend()的第一次调用。

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


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

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

可以访问字符串内容而不引起COW数据复制的操作包括:

  • 通过+连接
  • 通过<<输出
  • 使用basic_string作为标准库函数的参数

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

此外,一种实现可以提供各种非标准函数来访问字符串内容,而不触发COW数据复制。

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


注意:
ccoconst项访问器触发COW数据复制,因为它允许客户端代码获得指向数据的引用或指针,而不允许稍后由非const项访问器引发的数据复制使其无效

由于现在可以保证字符串是连续存储的,并且现在可以将指针指向字符串的内部存储(即&str[0]的工作原理与数组类似),因此不可能实现有用的COW。你将不得不为太多的东西做一个副本。即使只是在非常量字符串上使用operator[]begin()也需要一个副本。

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

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

对于大小约为300字节、N=2000的字符串,cow的速度稍快,使用的内存几乎减少了一个数量级。见下文,尺码以千磅为单位,跑步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