SGI STL 中的字符串
本文试图回答一些关于 SGI STL 中如何使用字符串的问题。草案标准中定义的字符串类有什么问题?
问题有几个,但最严重的问题与字符串中字符引用的生命周期规范相关。第二个委员会草案不允许表达式s[1] == s[2]其中 s 是非常量的字符串。这不仅仅是疏忽;当前的引用计数实现可能会导致更复杂的示例出现故障。它们甚至可能因s[1] == s[2]而失败,如果字符串 s 被另一个线程同时检查(仅仅是检查,不一定是修改)。现在很难确切地定义什么构成当前参考计数实现的正确用法。这个问题在 1997 年 7 月 C++ 标准化委员会会议上得到部分解决;解决方案是采用关于引用生命周期的更复杂的规则。不幸的是,这些新规则仍然没有解决多线程问题。
法国国家机构在第二个委员会草案的评论中指出了一个相关的问题。对于我们测试过的引用计数basic_string实现来说,下面的程序产生了错误的答案;问题在于,如果两个字符串共享一个公共表示,它们容易受到通过已存在的引用或迭代器进行修改。
# include <string>
# include <stdio.h>
main() {
string s("abc");
string t;
char & c(s[1]);
t = s; // Data typically shared between s and t.
c = 'z'; // How many strings does this modify?
if (t[1] == 'z') {
printf("wrong\n");
} else {
printf("right\n");
}
}
草案标准(以及常识)说更新对s的元素的引用只应修改s,而t不应修改;s和t有可能共享一个表示这件事是一个实现细节,不应影响程序行为。不过,鉴于basic_string的设计,对于引用计数实现来说要满足那个要求是非常困难的。
对引用计数实现来说避免这个问题唯一已知的方法是只要对那个字符串可能存在引用的情况下就将字符串标记为不可共享的。也就是说,每当程序获得对字符串的引用或迭代器(如通过operator[]或begin()获得),该特定字符串将不再使用引用计数;赋值和复制构造将复制字符串的元素而不是只复制一个指针。(我们不知道任何使用此技术并尝试保持线程安全的实现。)
这是一个彻底的解决方案:既然几乎所有字符串引用的方法都涉及引用或迭代器,所以这个解决方案意味着事实上唯一可以进行引用计数的字符串是那些从未被使用的字符串。那么在实践中,对basic_string不能实现可能在其他方面预期的性能提升,因为在所有特殊情况下都禁止引用计数。
一种不同的解决方案是完全放弃引用计数字符串的目标,并提供basic_string一个非引用计数实现。标准草案允许非引用计数实现,并且已经有多个供应商提供此实现。非引用计数basic_string的性能特征是可预测的,并且与vector<char>非常相似:例如,复制一个字符串始终是一个 O(N) 操作。
在此实现中,basic_string 不使用引用计数。
我一直使用引用计数实现,而且它工作正常。为什么我没有遇到问题?
当前实现大多数情况下都能正常工作:保留对字符串中字符的引用并不常见。(虽然保留对字符串的迭代器可能更频繁,并且完全相同的问题也适用于迭代器。)某些较不牵强的顺序程序也可能会失败,或者在不同平台上的表现不同。使用引用计数的多分线程应用程序basic_string可能会间歇性地失败,也许每隔几个月一次;这些间歇性故障很难重现和调试。但是,很大一部分多分线程客户端可能会偶尔发生故障,因此此类库完全不适用于多分线程使用。
那么我应该用什么来表示字符串?
有几个可能的选择,它们适用于不同的情况
- 绳索
- 使用绳索SGI STL 提供的软件包。它提供了所有可能需要的所有功能。它的接口类似于当前的标准草案,但有足够的差异以允许一个正确且线程安全的实现。对于所有不需要对字符串进行非常频繁的小更新的应用程序,它都应该表现得相当不错。这是唯一可以很好地扩展到非常长的字符串的替代方案,即 可以很容易地将电子邮件或文本文件表示为一个字符串。
缺点是
- 替换单个字符很慢。因此,在更新绳索时,STL 算法可能会很慢。(靠近起始位置的插入所花费的时间与替换单个字符所花费的时间大致相同,并且远少于其他字符串备选方案的对应插入所花费的时间。)
- 绳索实现扩展了当前的编译器技术。在短期内,可移植性和编译时间可能是一个问题。在非 SGI 平台上的 Pthread 性能将是一个问题,直到有人提供特定于机器的快速引用计数代码。(对于其他引用计数实现,这也可能是一个问题。)
- C 字符串
- 这可能是表示大量短字符串的最有效方式。对于小字符串来说,这是迄今为止最节省空间的备选方案。对于短字符串,<string.h>提供了一套管理此类字符串的实用工具。它们允许与 C 函数库轻松进行通信。主要缺点是
- 如果字符串很长,则连接和子字符串等操作比ropes昂贵得多。C 字符串无法很好地表示编辑器中的文本文件。
- 用户需要知道字符串表示之间的共享。如果通过复制指针分配字符串,则对一个字符串的更新可能会影响另一个字符串。
- C 字符串在存储管理方面没有提供帮助。这可能是一个主要问题,尽管垃圾回收器可以帮助缓解这个问题。
- vector<char>
- 如果字符串主要被视为一个字符数组,并且经常进行就地更新,那么将其表示为vector<char>或vector<wchar_t>是合理的。如果它将由 STL 容器算法修改,则也是如此。与 C 字符串不同,vector 自动处理内部存储管理,并且修改字符串长度的操作通常更方便。
缺点是
- Vector 赋值比 C 字符串指针赋值昂贵得多;共享字符串表示的唯一方法是传递指向 vector 的指针或引用。
- 对整个字符串执行的大多数操作(例如赋值、连接)都不适合长字符串。
- 一些标准字符串操作(例如连接和子字符串)不会提供常规语法,并且必须使用通用 STL 算法来表达。这通常并不难。
- 转换为 C 字符串目前很慢,即使是对于短字符串也是如此。这可能会在未来的实现中发生改变。
关于mstring.h,它随 SGI 的 7.1 编译器一起提供?
此软件包是对免费提供的 Modena 字符串软件包的最小改编。它的目的是权宜之计。我们无意进一步开发它。它与其他试图符合草案标准的实现共享了一些引用生存期问题。它确切的语义从未得到很好的定义。在罕见的情况下,它将对单线程应用程序具有意外的语义。它在上面给出的示例中失败。我们强烈建议不要在多线程应用程序中使用它。
版权所有 © 1999 Silicon Graphics, Inc. 保留所有权利。
商标信息