C++11中COW std::string实现的合法性
Legality of COW std::string implementation in C++11
我一直认为写时复制不是在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字符串,调用非
const
operator[]
将需要制作一个副本(并使引用无效),这是[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
项目访问器,代价是使用微妙而复杂的无效规则:
”引用
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
- 如果没有malloc,链表实现将失败
- 如何在c++中实现处理器调度模拟器
- 如何在c++中使用引用实现类似python的行为
- 实现无开销push_back的最佳方法是什么
- 使用简单类型列表实现的指数编译时间.为什么
- 如何在BST的这个简单递归实现中消除警告
- 实现一个在集合上迭代的模板函数
- 我应该实现右值推送功能吗?我应该使用std::move吗
- 如何正确实现和访问运算符的各种自定义枚举器
- C++Union/Struct位域的实现和可移植性
- 这个极客对极客的trie实现是否存在内存泄漏问题
- 在c++中实现LinkedList时,应出现未处理的错误
- 为左值和右值的包装器实现C++范围
- 使用模板进行堆栈实现; "name followed by :: must be a class or namespace"
- 使用GSoap实现ONVIF
- 在用于格式4的arm模拟器中实现功能时的一个问题
- 用于AVX的ln(x)的实现,m256
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 在C++中,如何在类和函数(可能是模板化的)的头中编写完整的实现
- C++11中COW std::string实现的合法性