C++11 中 COW std::string 实现的合法性
Posted
技术标签:
【中文标题】C++11 中 COW std::string 实现的合法性【英文标题】:Legality of COW std::string implementation in C++11 【发布时间】:2012-08-25 08:22:09 【问题描述】:据我了解,写时复制不是在 C++11 中实现符合标准的 std::string
的可行方法,但最近在讨论时发现自己无法直接支持该声明。
C++11 不承认基于 COW 的 std::string
实现我是否正确?
如果是这样,这个限制是否在新标准的某处明确说明(在哪里)?
或者说这个限制是隐含的,因为新要求对std::string
的综合影响排除了std::string
的基于COW 的实现。在这种情况下,我会对“C++11 有效禁止基于 COW 的 std::string
实现”的章节风格派生感兴趣。
【问题讨论】:
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 【参考方案1】:这是不允许的,因为根据标准 21.4.1 p6,迭代器/引用的失效只允许用于
——作为任何标准库函数的参数,引用 以非常量 basic_string 作为参数。
——调用非常量 成员函数,除了 operator[]、at、front、back、begin、rbegin、 结束,然后撕裂。
对于 COW 字符串,调用非 const operator[]
将需要制作副本(并使引用无效),这在上面的段落中是不允许的。因此,在 C++11 中使用 COW 字符串不再合法。
【讨论】:
一些理由:N2534 -1 逻辑不成立。在 COW 复制时,没有可以使无效的引用或迭代器,进行复制的重点是现在正在获取此类引用或迭代器,因此复制是必要的。但可能仍然是 C++11 不允许 COW 实现。 @Cheersandhth.-Alf:如果允许 COW,则逻辑如下所示:std::string a("something"); char& c1 = a[0]; std::string b(a); char& c2 = a[1];
c1 是对 a 的引用。然后你“复制”一个。然后,当您第二次尝试获取引用时,它必须进行复制以获取非常量引用,因为有两个字符串指向同一个缓冲区。这将不得不使第一次引用无效,并且违反上面引用的部分。
@Cheersandhth.-Alf,根据this,至少 GCC 的 COW 实现确实完全按照 DaveS 的说法。因此,至少标准禁止了这种风格的 COW。
@Alf:这个答案认为非常量 operator[]
(1) 必须进行复制,并且 (2) 这样做是非法的。你不同意这两点中的哪一点?查看您的第一条评论,似乎实现可以共享字符串,至少在此要求下,直到它被访问,但读取和写入访问都需要取消共享它。这是你的理由吗?【参考方案2】:
Dave S 和gbjbaanb 的答案正确。 (而 Luc Danton 的说法也是正确的,尽管它更多的是禁止 COW 字符串的副作用,而不是禁止它的原始规则。)
但为了消除一些混乱,我将添加一些进一步的说明。各种 cmets 链接到 a comment of mine on the 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 中是不允许的C++11。
【讨论】:
在调用.data()
(以及每次返回指针、引用或迭代器时)取消共享的实现不会遇到该问题。 IE。 (不变)缓冲区在任何时候都是不共享的,或者与没有外部引用共享。我以为您打算将有关此示例的评论作为非正式的错误报告作为评论,非常抱歉误解了它!但是,正如您通过考虑我在此处描述的这种实现所看到的那样,当 noexcept
要求被忽略时,它在 C++11 中运行良好,该示例并没有说明任何形式。如果你愿意,我可以提供代码。
如果您在几乎每次访问字符串时取消共享,那么您将失去共享的所有好处。 COW 实现必须是实用,标准库才能将其用作std::string
,我真诚地怀疑您能否演示一个满足 C++11 失效要求的有用、高性能的 COW 字符串。所以我坚持认为最后一分钟添加的noexcept
规范是禁止 COW 字符串的结果,而不是根本原因。 N2668 似乎非常清楚,您为什么继续否认那里概述的委员会意图的明确证据?
另外,请记住 data()
是一个 const 成员函数,因此与其他 const 成员同时调用必须是安全的,例如,与另一个创建副本的线程同时调用 data()
细绳。因此,对于 每个 字符串操作,甚至是 const 操作,您将需要互斥锁的所有开销,或者无锁可变引用计数结构的复杂性,毕竟您只会得到如果您从不修改或访问您的字符串,则共享,那么许多字符串的引用计数将为 1。请务必提供代码,请随意忽略noexcept
保证。
现在只是拼凑一些代码,我发现有 129 个basic_string
成员函数,外加免费函数。抽象成本:这个现成的非优化的新鲜零版本代码在 g++ 和 MSVC 下都会慢 50% 到 100%。它不具备线程安全性(我认为,利用shared_ptr
很容易)并且仅支持对字典进行排序以用于计时目的就足够了,但是模数错误证明了引用计数basic_string
是允许的,除了对于 C++ noexcept
要求。 github.com/alfps/In-principle-demo-of-ref-counted-basic_string
让我们continue this discussion in chat。【参考方案3】:
确实,CoW 是一种可接受的制作更快字符串的机制...但是...
它使多线程代码变慢(所有锁定以检查您是否是唯一一个编写代码的人在使用大量字符串时会降低性能)。这是几年前 CoW 被杀的主要原因。
其他原因是[]
运算符将返回字符串数据,而不会保护您覆盖其他人期望不变的字符串。这同样适用于c_str()
和data()
。
快速谷歌说,多线程基本上是reason it was effectively disallowed(未明确)。
提案说:
建议
我们建议安全地进行所有迭代器和元素访问操作 并发执行。
我们正在提高操作的稳定性,即使在顺序代码中也是如此。
此更改实际上禁止了写时复制实现。
紧随其后
由于切换而导致的最大潜在性能损失 写时复制实现是增加内存消耗 适用于具有非常大的以读取为主的字符串的应用程序。然而,我们 相信对于这些应用,绳索是一种更好的技术 解决方案,并建议考虑将绳索提案纳入 图书馆 TR2。
Ropes 是 STLPort 和 SGIs STL 的一部分。
【讨论】:
operator[] 问题并不是真正的问题。 const 变体确实提供了保护,而非 const 变体始终可以选择在那个时候执行 CoW(或者非常疯狂并设置页面错误来触发它)。 +1 解决问题。 不包括 std::cow_string 类和 lock_buffer() 等,这很愚蠢。很多时候我知道线程不是问题。实际上,通常情况下。 我喜欢替代方案的建议,例如绳索。我想知道是否还有其他可用的替代类型和实现。【参考方案4】:从 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。 对不起,我累了。它仅说明如果当前共享缓冲区,调用 .data() 必须触发 COW 复制。仍然是有用的信息,所以我让赞成票站立。【参考方案5】:C++11 及更高版本中是否禁止使用 COW basic_string
?
关于
”我是否正确 C++11 不承认基于 COW 的
std::string
实现?
是的。
关于
”如果有,这个限制是否在新标准的某处明确说明(在哪里)?
几乎直接,通过需要对字符串数据进行 O(n) 次物理复制的许多操作的恒定复杂性要求在 COW 实现中。
例如,对于成员函数
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()
、@987654327 @、end()
或 rend()
使引用字符串项的引用、指针和迭代器无效,即可能导致 COW 数据复制。在 C++11 中删除了此支持。
C++11 失效规则也禁止 COW 吗?
在撰写本文时被选为解决方案的另一个答案中,该答案被大力支持,因此显然相信,它断言
” 对于 COW 字符串,调用非
const
operator[]
将需要制作副本(并使引用无效),这是 [C++ 上面的 [quoted] 段落所不允许的11 §21.4.1/6]。因此,在 C++11 中使用 COW 字符串不再合法。
该断言在两个主要方面是不正确和误导的:
它错误地表示只有非const
项目访问者需要触发 COW 数据复制。
但const
项目访问器也需要触发数据复制,因为它们允许客户端代码形成引用或指针,(在 C++11 中)不允许以后通过可触发 COW 数据复制的操作使其无效。李>
它错误地认为复制 COW 数据会导致引用失效。
但在正确的实现中,COW 数据复制、取消共享字符串值是在任何引用无效之前完成的。
要了解basic_string
的正确 C++11 COW 实现如何工作,当忽略使此无效的 O(1) 要求时,请考虑一个字符串可以在所有权策略之间切换的实现。字符串实例以策略共享开始。启用此策略后,将没有外部项目引用。该实例可以转换为唯一策略,并且它必须在可能创建项目引用时这样做,例如调用.c_str()
(至少如果这会产生指向内部缓冲区的指针)。在多个实例共享值所有权的一般情况下,这需要复制字符串数据。在转换为 Unique 策略之后,实例只能通过使所有引用无效的操作(例如分配)转换回 Sharable。
因此,虽然该答案的结论(即排除了 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++ 的标准库实现中的一种。特殊的 C++03 COW 支持实现了实际效率,特别是使用 const
项目访问器,但代价是微妙、复杂的无效规则:
” 引用
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) 要求被忽略怎么办?
如果 C++11 的常量时间要求在例如operator[]
被忽略,那么 basic_string
的 COW 在技术上是可行的,但很难实现。
可以访问字符串内容而不引起 COW 数据复制的操作包括:
通过+
连接。
通过<<
输出。
使用basic_string
作为标准库函数的参数。
后者是因为允许标准库依赖于特定于实现的知识和构造。
另外,一个实现可以提供各种非标准函数来访问字符串内容,而不会触发 COW 数据复制。
一个主要的复杂因素是,在 C++11 中,basic_string
项目访问必须触发数据复制(取消共享字符串数据),但要求不抛出,例如C++11 §21.4.5/3 “抛出: 什么都没有。”。所以它不能使用普通的动态分配来创建一个新的缓冲区来复制COW数据。解决这个问题的一种方法是使用一个特殊的堆,其中可以保留内存而不实际分配,然后为每个对字符串值的逻辑引用保留必要的数量。在这样的堆中保留和取消保留可以是常数时间,O(1),分配一个已经保留的数量可以是noexcept
。为了符合标准的要求,对于这种方法,似乎每个不同的分配器都需要一个这样的特殊的基于预留的堆。
注意事项:
¹ const
项目访问器触发 COW 数据复制,因为它允许客户端代码获取指向数据的引用或指针,不允许由例如触发的后续数据复制使其无效。非const
项目访问者。
【讨论】:
"您的示例是 C++11 实现不正确的一个很好的示例。可能它对于 C++03 是正确的。" 是的 就是这样例子的重点。它显示了一个在 C++03 中合法的 COW 字符串,因为它不会破坏旧的迭代器失效规则,并且在 C++11 中是不合法的,因为它确实破坏了新的迭代器失效规则。这也与我在上面评论中引用的声明相矛盾。 当您调用s.c_str()
时,如果s
尚未共享怎么办?你如何处理c_str()
返回的指针稍后失效?字符串需要记住是否曾经获取过任何引用,即使它是唯一拥有的,然后永远不允许共享。这在现实世界中是不切实际的。
如果你说可共享>不是最初共享>,我不会争论。说某些东西最初是共享的只是令人困惑。与自己共享?这不是这个词的意思。但我重复一遍:您试图争辩说 C++11 迭代器无效规则并没有禁止一些在实践中从未使用过的假设 COW 字符串(并且会具有不可接受的性能),而实际上它们确实禁止了那种实践中使用的 COW 字符串,有些学术性和无意义。
您提议的 COW 字符串很有趣,但我不确定它会有多大有用。 COW 字符串的要点是仅在写入两个字符串的情况下复制字符串数据。当发生任何用户定义的读取操作时,您建议的实现需要复制。即使编译器知道它只是一次读取,它仍然必须复制。此外,复制唯一字符串将导致其字符串数据的副本(可能是共享状态),这再次使 COW 变得毫无意义。因此,如果没有复杂性保证,您可以编写... 非常糟糕的 COW 字符串。
@JonathanWakely:(1)您的报价不是问题。这里是一个问题:“我对 C++11 不承认基于 COW 的 std::string 实现是否正确?如果是这样,这个限制是否在新标准的某个地方(在哪里)明确说明?” (2) 您认为 COW std::string
在无视 O(1) 要求时效率低下,这是您的意见。我不知道性能可能是什么,但我认为提出这个断言更多是为了它的感觉,它传达的氛围,而不是与这个答案的任何相关性。【参考方案6】:
由于现在可以保证字符串是连续存储的,并且您现在可以获取指向字符串内部存储的指针(即 &str[0] 的工作方式与数组一样),因此不可能一个有用的 COW 实现。您将不得不为太多事情制作副本。即使只是在非常量字符串上使用operator[]
或begin()
也需要一个副本。
【讨论】:
我认为 C++11 中的字符串保证是连续存储的。 过去你必须在所有这些情况下进行复制,这不是问题...... @mfontanini 是的,但以前没有 尽管 C++11 确实保证字符串是连续的,但这与禁止 COW 字符串是正交的。 GCC 的 COW 字符串是连续的,所以很明显,您声称 “不可能做出有用的 COW 实现” 是虚假的。 @supercat,请求后备存储(例如通过调用c_str()
)必须是 O(1) 并且不能抛出,并且不能引入数据竞争,所以如果满足这些要求是非常困难的你懒惰地连接。在实践中,唯一合理的选择是始终存储连续数据。【参考方案7】:
我一直在想不可变的奶牛:一旦奶牛被创建,我只能通过另一头奶牛的赋值来改变,因此它符合标准。
我今天有时间尝试了一个简单的比较测试:一个大小为 N 的映射,由字符串/牛键入,每个节点都保存映射中的一组所有字符串(我们有 NxN 个对象)。
字符串大小约为 300 字节且 N=2000 头牛稍快一些,并且使用的内存几乎减少了一个数量级。见下文,size 以 kbs 为单位,run 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
【讨论】:
以上是关于C++11 中 COW std::string 实现的合法性的主要内容,如果未能解决你的问题,请参考以下文章
在 C++11 及更高版本中,std::string::operator[] 是不是进行边界检查?
如何在 C++11 中将 std::string 转换为 std::u32string?