不可变字符串 vs std::string

69
我最近阅读了一些关于不可变字符串的资料(为什么Java和.NET中的字符串不能是可变的?)以及(为什么.NET的String是不可变的?),还有一些关于为什么D语言选择使用不可变字符串的内容。这似乎有许多优点。

  • 可以轻而易举地实现线程安全
  • 更加安全
  • 在大多数情况下,内存利用率更高
  • 廉价的子字符串(标记化和切片)

更不用说大多数新语言都采用了不可变字符串,例如 D 2.0、Java、C#、Python 等等。

C++是否从不可变字符串中受益?

在 C++(或 C++0x)中是否有可能实现一个不可变字符串类,具备所有这些优点?


更新:

有两个尝试实现不可变字符串:const_string fix_str。 它们都已经半个十年没有更新了。它们是否有用?为什么const_string从未被纳入boost?


43
你提出了一个非常详尽且有说服力的论点,BlueRaja。 - peterchen
6
蓝色拉杰并没有提出一个实际的论点,正如大家所清楚指出的那样。但他可能是正确的,因为C ++可能对于纯粹的不可变字符串的尝试来说过于混合了。当然,这更多地反映了C ++文化而不是语言本身。 - Steven Sudit
5
异议!Ruby的字符串不是不可变的! - Notinlist
1
它们自2005年以来就没有更新过,但是没有报告很多错误,所以我认为使用它们是可以的。 - Franklin Yu
11个回答

54

我发现这个帖子中大多数人并不真正理解immutable_string是什么。它不仅仅是关于constness(不可变性)。immutable_string的真正威力在于性能(即使在单线程程序中)和内存使用。

想象一下,如果所有字符串都是不可变的,并且所有字符串都像这样实现:

class string {
    char* _head ;
    size_t _len ;
} ;

如何实现一个子字符串操作?我们不需要复制任何字符。我们只需要分配 _head_len,然后子字符串就共享与源字符串相同的内存段。

当然,只有这两个数据成员是无法真正实现不可变字符串的。真正的实现可能需要一个引用计数(或飞行权重)的内存块。像这样:

class immutable_string {
    boost::fly_weight<std::string> _s ;
    char* _head ;
    size_t _len ;
} ;

在大多数情况下,无论是内存还是性能,与传统字符串相比,使用不可变字符串都会更好,特别是当您知道自己在做什么时。

当然,C++也可以从不可变字符串中受益,并且拥有一个不可变字符串很好。我已经查看了Cubbi提到的boost::const_stringfix_str,它们应该就是我所说的内容。


26
作为一种观点:
  • 是的,我很想要一个不可变的 C++ 字符串库。
  • 不,我不希望 std::string 是不可变的。

这真的值得做成标准库特性吗?我认为不值得。使用 const 可以给你本地不可变的字符串,并且系统编程语言的基本特性意味着你确实需要可变的字符串。


4
在C++中,我遇到的最接近不可变字符串的类是“span”类,它有两个const指针,一个用于开头,一个用于结尾。它不管理内存,但支持通常的实用函数(例如查找等)。因此,它在解析方面非常有用。 - Steven Sudit
2
@StevenSudit:许多大型项目都有这个,尽管它通常被称为“stringref”或类似的名称。 - Mooing Duck
3
@MooingDuck 那是真的。谷歌称其为StringPiece - Steven Sudit
2
在C++17中还有string_view,它似乎只能是const。 - pilkch

9
我的结论是,C++不需要不可变模式,因为它具有const语义。
在Java中,如果你有一个名为Person的类,并使用getName()方法返回该人的String name,则你唯一的保护是不可变模式。如果没有这个模式,你就必须彻夜地克隆你的字符串(就像对于不是典型值对象但仍需要受到保护的数据成员一样)。
在C++中,你有const std::string& getName() const。因此,你可以编写SomeFunction(person.getName()),其中subject就像void SomeFunction(const std::string& subject)一样。
没有发生复制
如果任何人想要复制,他可以自由地这样做
这种技术适用于所有数据类型,不仅仅是字符串

4
更正!不可变字符串在多线程程序中非常有用,因为它们对处理并发没有任何额外开销。而且大多数情况下,您不会编辑字符串,而是简单地替换它们。 - Notinlist

3
你肯定不是唯一一个这么想的人。事实上,Maxim Yegorushkin 创作了一个名为 const_string 的库,似乎旨在将其纳入 boost 中。还有一个更新一点的库,Roland Pibinger 创作的 fix_str。我不确定在运行时进行完整字符串内部化会有多棘手,但大多数优点在必要时都是可以实现的。

3
我认为这里没有一个明确的答案。这是主观的——如果不是因为个人口味,那么至少是因为大多数人处理的代码类型不同。(尽管如此,这是一个有价值的问题。)
当内存便宜时,不可变字符串非常好——当C++开发时并非如此,也并非所有C++目标平台都是这样。(另一方面,在更受限制的平台上,C比C++更常见,所以这个论点不够有力。)
您可以在C++中创建一个不可变字符串类,并使其与std::string基本兼容——但与具有专用优化和语言功能的内置字符串类进行比较仍将失败。 std::string是我们得到的最好的标准字符串,因此我不想看到任何对它的干扰。尽管如此,我很少使用它;std::string从我的角度来看有太多缺点。

3
如果 std::string 是最好的标准字符串,但你很少使用它,因为它有太多的缺点,那么你会使用什么? - Evgeniy Berezovsky
3
因为有超过10年的积累库、更好的本地 API 互操作性(包括 wchar_t/char 转换),所以我选择使用 CString(请不要打死我)。当时,CString 的明确定义的写时复制机制也比 std::string 更具性能保证优势。 - peterchen
@peterchen 在MFC或WTL中使用CString? - Mike
1
@Mike 由于它们具有源代码兼容性,因此可以使用MFC和ATL版本,但是它们是两个不匹配的实现。始终拥有库的“ATL”和“MFC”版本是一个主要的WTF。 - peterchen
1
不可变字符串至少可以追溯到1970年代末,如果不是更早的话;我认为当时内存并不便宜。在Applesoft BASIC、Commodore BASIC或许多其他实现中,字符串数组中的每个元素都将保存一个两字节指针和一个一字节长度;字符串数据本身将存储在一个没有其他开销的池中。像A$(4)=A$(6)这样的语句只会复制长度和指针;它不必复制任何数据。微软的垃圾回收算法实现得不好(非常慢),但代码有可能确定何时进行GC循环... - supercat
1
...将很快到来,并使用一些PEEK和POKE为垃圾收集器添加“世代”,或者使用更高效的第三方GC。虽然程序存储固定字符串集合并每个字符串增加一个字节的开销很常见,但对于可变长度的混合字符串数组,我认为没有更少的开销。不可变字符串的问题在于GC必须能够找到它们,在某些语言中这是可行的,但在C++中就不那么好了。 - supercat

2
const std::string

就是这样。一个字符串字面值也是不可变的,除非你想进入未定义的行为。

编辑:当然,这只是故事的一半。一个常量字符串变量是没有用的,因为你不能让它引用一个新的字符串。对一个常量字符串的引用会做到这一点,但是C++不允许像Python等其他语言中那样重新分配引用。最接近的方法是使用指向动态分配字符串的智能指针。


2
你需要更多的东西,例如你希望 std::string::replace 返回一个修改后的副本而不是导致编译错误。 - peterchen
1
@peterchen -> const std::string orig; const std::string copy = std::string(orig).replace(...); - 一个不可变字符串会做得更好吗? - Edward Strange
在我看来,给一个新字符串赋值就是对该字符串进行了改变。根据我对 API 的记忆,拥有这种结构的 API 也是这样处理的。你真正想要的听起来更像是可分配的引用,而不是让一个 const 字符串可分配。我认为,使用智能指针可能是更好的解决方案。我有时也会发现 const std::string 变量很有用,所以在这一点上,我们可能需要有所不同。 - Edward Strange
3
这不是一个不可变对象的正确接口,它包含两个语句而不是一个,这是实现细节泄漏到调用代码中?--- 对象应该使正确的事情变得容易,错误的事情变得困难(或者不可能)。在复制和替换之间需要插入“不要将此字符串显示给其他线程”的注释吗?之后再加上“现在可以”吗? --- 我同意 const std :: string 是一个近似的选择,但没有一些好处。 - peterchen
@Peter:如果语言支持两种replace类型会很好,一种是当前的replace,另一种是replaced,后者操作常量引用并返回已进行替换的副本。后者可以避免重复复制所有东西。但是,只要我们缺少这样的函数,我们就必须使用Noah的解决方法,这是一个合理的替代方法。更好的答案是完全支持std::string的不可变变体。 - Steven Sudit
@CrazyEddie:不是复制源字符串,而是直接使用它(以不可变的方式)。 - SasQ

1
不可变字符串非常好,因为每当需要创建新字符串时,内存管理器总是能够确定每个字符串引用的位置。在大多数平台上,这种能力的语言支持可以以相对较小的成本提供,但在没有内置此类语言支持的平台上,这就更加困难了。
例如,如果想要在x86上设计支持不可变字符串的Pascal实现,则需要使字符串分配器能够遍历堆栈以查找所有字符串引用;其唯一的执行时间成本将是需要一致的函数调用方法[例如,不使用尾调用,并且每个非叶子函数都要维护一个帧指针]。使用new分配的每个内存区域都需要有一个位来指示它是否包含任何字符串,并且那些包含字符串的内存区域需要有一个索引指向内存布局描述符,但这些成本将相当小。
如果GC无法遍历堆栈,则需要代码使用句柄而不是指针,并且在局部变量进入作用域时创建字符串句柄,并在它们超出作用域时销毁句柄。开销更大。

0

Qt 也使用了带有写时复制的不可变字符串。
对于性能提升到底有多少,这还存在一些争议,尤其是在使用良好的编译器时。


4
我不会称复制-写入字符串为不可变的。不可变的字符串是复制-写入字符串的一个子集。也就是说,几乎所有不可变字符串能够做到的复制-写入字符串都能够做到,但反之则不然。正是这些额外的能力使得在并发环境下使用复制-写入字符串很糟糕。 - deft_code
@David:Qt使用线程安全的COW;它使用原子整数来进行引用计数,并且使用自己的锁定机制。 - CMircea
1
@Caspin - 是的,但如果你要使用不可变字符串,最好使用COW来有效利用它们。 - Martin Beckett
1
@iconiK:这就是为什么有注释“(...或者在库内部)”的原因。问题在于需要进行锁定操作,而这可能是一项昂贵的操作。隐藏这一操作对用户来说意味着在用户代码中出错的机会较少,但并不能减少成本。如果将其与Java中的不可变字符串进行比较,你可以复制引用并知道它们永远不会被更改,你可以以几乎没有任何成本创建修改(在分代GC中分配是快速的--只需10个CPU指令)。 - David Rodríguez - dribeas
1
Copy-on-write并不需要锁,它只是意味着看起来修改实例的操作实际上将其指向一个新缓冲区,保留原始数据。替换指针几乎总是原子的。隐藏的成本在于管理原始数据的生命周期,通常通过引用计数来完成。即使使用交错操作,这种计数也很昂贵,这就是为什么std:string实现确实已经摆脱了它的原因。在像C#这样的GC语言中,这不是问题,因此我们有不可变的字符串,但没有COW语义。 - Steven Sudit
显示剩余3条评论

0

常量字符串在值语义方面意义不大,共享也不是C++的最大优势...


或许我们在讨论“值语义”时指的是不同的事情。C#字符串总是通过透明层级的间接性(引用语义)进行处理,而C ++字符串则不是(值语义)。 - fredoverflow
1
或许吧。在 C# 中,实际值类型(如 int)继承自 System.ValueType 并作为副本传递,而引用类型则是按引用传递,并(通常)按引用比较。虽然 C# 字符串是引用类型,但它们具有值语义,因为它们是不可变的并按内容而非地址进行比较。在 C++ 中,std::string 是一个值类型,但它包含对可变缓冲区的引用(实际上是指针)。因此,传递 C++ 字符串的副本会调用复制构造函数来复制缓冲区,而传递 const 引用可以避免开销。希望讲得更清楚了。 - Steven Sudit

-1

Ruby 中的字符串是可变的。

$ irb
>> foo="hello"
=> "hello"
>> bar=foo
=> "hello"
>> foo << "world"
=> "helloworld"
>> print bar
helloworld=> nil
  • 线程安全很简单

在编写代码时,我倾向于忘记安全性的论点。如果您想要实现线程安全,锁定它或不要触及它。C++并不是一种方便的语言,您需要有自己的约定。

  • 更安全

不,在进行指针算术和对地址空间的未受保护访问时,就可以放弃安全了。更安全的是针对无心之失的糟糕编码。

  • 在大多数情况下更占用内存

除非您实施CPU密集型机制,否则我看不出如何做到更加占用内存。

  • 廉价的子字符串(标记化和切片)

这将是一个非常好的优点。可以通过引用带有后向引用的字符串来实现,其中对字符串的修改会导致副本。标记化和切片变得免费,突变变得昂贵。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接