SGI 的 STL 版本中包含的 Rope 容器类型大致基于施乐雪松环境中的 Rope 或 C “cord”,如 Boehm、Atkinson 和 Plass 在《Rope:字符串的替代方案》中描述的,Software Practice and Experience 25,12(1995 年 12 月),第 1315-1330 页。
Rope 表示为指向_Rope_RopeRep结构的指针,该结构表示一个树节点。每个树节点对应一段 Rope。虽然我们称之为“树节点”,但这样一段 Rope 可以在不同的 Rope 之间共享,如果相应的子串重复出现,甚至可以在同一个 Rope 中被重用。因此,Rope 实际上表示为有向无环图。尽管如此,我们仍将继续称其为树,因为它既是通常情况,也是更直观的理解方式。
每个树节点包含一个 size 字段,给出 Rope 段的长度,一个 depth 字段,指定以该节点为根的树的深度(或高度),一个布尔字段,表示子树是否已平衡,以及一个标签字段,表示_Rope_RopeRep的四个变体或子类中的哪一个用于表示列表(平衡位实际上仅对串联树节点有效,见下文)。
可以使用虚函数和/或 RTTI 来代替标签字段。我们选择不沿该途径进行,因为标签字段可以比 vtable 指针小得多,而且在这种情况下,基于标签的代码可能也更快。
的 4 个子类_Rope_RopeRep是
只有连接节点才有非零的深度字段。由于我们对绳索深度强制规定了一个静态最大值,因此深度字段有保证能放入一个字节。
绳子实现可以用两种不同的方法编译。通常情况下,将不会定义 __GC 。在这种情况下,每个树节点也会包含一个引用计数字段。这会追踪引用该树节点的绳变量、连接节点或子串节点的数量。(我们稍后会看到,还会包括来自某些迭代器的引用。)当树节点的引用计数变为零时,树节点会被释放,并且任何子树的引用计数相应地递减。
在某些情况下,引用计数还会用于允许就地更新绳索。如果从绳索根节点 R 到持有特定字符的叶节点 L 的路径上的所有树节点的引用计数均为 1,那么 L 只在 R 中出现一次,不会出现在任何其他绳索中。因此,可以通过就地更新 L 来安全地更新 R。
如果通过已定义__GC编译绳子实现,它会假定存在一个底层垃圾回收器,且不可访问的树节点会自动回收。在这种情况下,必须使用合适的垃圾回收分配器对绳索进行实例化,并且不保留任何引用计数。因此,上述针对就地更新的优化也不会实现。由于即使非破坏性更新也只会复制绳子的一部分,并且由于许多绳子客户端将它们纯用作不可变字符串,所以这通常不会造成严重的损失。但对于某些应用程序而言可能并非如此。
本节的剩余部分假设未定义 __GC,且使用了引用计数。
由于不同线程可以并发地复制、更新或销毁不同绳索共享的绳索节点,因此必须以原子方式更新引用计数。这是实现执行的唯一显式同步,因为引用计数是潜在共享数据结构中唯一会被更新的部分。
引用计数更新所需的同步可能会占据绳索操作所需时间的很大一部分。无论何时可用原子添加操作,都应根据原子添加操作实现引用计数更新。很重要,引用计数递减操作不仅要以原子方式递减计数,还必须作为原子操作的一部分返回结果。如果在操作的原子部分之外执行零测试,那么相同的树节点可能会被释放两次。
在 Irix 和 win32 平台上,当前实现使用原子添加操作维护引用计数。还提供了基于 PThread 互斥的一个更通用的实现。但对于广泛使用绳索的应用程序,它不太可能提供最佳性能。
选择此表示法是因为它使实现相对干净,并且无实例的情况相当高效。在绳索中仅存储分配器实例的替代方案会为许多内部函数添加额外的分配器参数。对于没有不同实例的分配器类型来说,消除这种开销很困难。
将由单个叶组成的短绳连接到绳索右边的特殊情况得到特殊处理,该绳索要么是叶,要么是连接节点,其右子代是叶。在这种情况下,如果所讨论的叶足够短,我们可能会分配一个新叶来保存这两个叶的合并内容,或者在正确的情况下,甚至就地更新左操作数。为了允许破坏性更新,保存叶字符的实际数组以 8 为增量增长。
例如,绳索“abcedefghigklmnopqrstuvwxy”可能连接到“z”,如下图所示:
特殊处理此种情况确保了通过重复地将短字符串连接到右边创建的绳索将由最小大小的叶子构成,因此可以有效地进行储存和处理。对同一位置重复插入字符时会有类似的效果。
虽然连接操作非常有效,且与树的形状无关,但一些其他操作(例如获取第i个字符)如果表示绳索的树大约平衡,则会更有效。可以通过明确调用balance成员函数或者隐式作为连接操作的结果对绳索进行重新平衡。当深度有溢出危险时,或者绳索同时超过较小的深度阈值(当前为 20)并低于最小长度(当前为 1000),则会隐式重新平衡绳索。
重新平衡操作按上面引用的论文中描述的那样进行。该操作是非破坏性的;以前与其他绳索共享的重新平衡部分在该操作后不再共享。在对绳索进行平衡时,所有深度足够小以其长度而不会进行进一步平衡操作的连接节点中都会设置平衡位。带有已设置平衡位的树节点不会由进一步的平衡操作检查。因此,平衡操作倾向于不重新平衡同一子字符串。
尽管如此,重新平衡的最坏情况成本在字符串中是线性的。但是,观察到的行为是重新平衡通常仅消耗一小部分的运行时间。事实上,通过重复连接构建长度为N的绳索的所有自然方式都需要在 N 中线性总时间。违反此规则是可以的,但并不好用,设计连接顺序。
子字符串操作根据表示绳索根的树节点的不同而执行。该操作是递归的
正常生成的迭代器begin()和end()操作生成 const_iterators,即不允许更新基本绳索的迭代器。这样的迭代器基本上包含三类信息
此实现与 C“cord”程序包中使用的实现有很大不同。我们不会在迭代器中为从根节点到当前节点的完整路径预留空间。此更改是为了适应假设小迭代器可以按值廉价传递的 STL 代码。我们尝试通过提供迭代器赋值操作进一步帮助此代码,除非初始化了迭代器赋值操作的缓存部分,否则此操作不会复制它。
迭代器中的字符缓冲区是必需的,因为没有其他地方可以缓存为函数节点的评估返回的字符。(使用垃圾回收器时,这是一个小问题,因为在函数对象中维护这样的缓存非常有效。如果没有垃圾回收器,这会要求对缓存访问使用锁,这通常是不可接受的。)
可变迭代器主要的优点在于,它们还包含指向绳本身的指针(即指向树节点指针的指针),因而可用于执行更新,并且,绳根指针包含在引用计数中。在这种情况下,绳根指针是多余的,但可以用它来检测绳中的变更,以便使迭代器中的缓存信息失效。当且仅当绳本身不再指向与迭代器中的绳根指针相同的节点时,绳才已改变。迭代器包含在引用计数中的事实确保了根节点不会被丢弃和重用。它还能够防止破坏性更新绳,而迭代器必须保持有效。