【问题标题】:How to compare a "basic_string" using an arbitary locale如何使用任意语言环境比较“basic_string”
【发布时间】:2014-04-24 21:30:24
【问题描述】:

我重新发布了我今天早些时候提交的一个问题,但我现在引用一个具体的例子来回应我收到的反馈。原题可以在here找到(注意不是家庭作业):

我只是想确定 C++ 是否无法对 basic_string 对象执行(有效的)不区分大小写比较,该对象还考虑了任意 locale 对象。例如,似乎不可能编写如下高效的函数:

bool AreStringsEqualIgnoreCase(const string &str1, const string &str2, const locale &loc);

根据我目前的理解(但有人可以确认这一点),此函数必须为给定的locale 调用ctype::toupper()collate::compare()(一如既往地使用use_facet() 提取)。但是,因为 collate::compare() 特别需要 4 个指针参数,所以您需要为需要比较的每个字符传递这 4 个参数(在第一次调用 ctype::toupper() 之后),或者先将两个字符串都转换为大写,然后再创建一个单次致电collate::compare()

第一种方法显然效率低下(为每个测试的字符传递 4 个指针),第二种方法要求您将两个字符串全部转换为大写(需要分配内存和将两个字符串不必要地复制/转换为大写) .我对此是否正确,即不可能有效地做到这一点(因为没有办法绕过collate::compare())。

【问题讨论】:

  • 我看到你还在张贴巨大的文字墙。
  • 这里是the help for formatting。在您撰写问题时提供了指向它的链接,因此您今天已经看到它并忽略了它两次。也许现在是时候看看了,是吗?
  • 我改进了当前帖子的格式,但你真的应该学习如何为以后的问题做。
  • 粗略阅读collate::compare 告诉我你可能误解了它的参数。特别是,您不需要“为每个字符传递 4 个参数”。它的参数只是两个字符串,每个字符串都有一个指向其开始和结束的指针(作为以 0 结尾的字符串的替代方法或提供长度)。
  • @Lightness:您的 cmets 已被记录。

标签: c++


【解决方案1】:

试图以一致的方式与世界上所有的书写系统打交道的一个小烦恼是,实际上你认为你知道的关于字符的所有内容实际上都是正确的。这使得做“不区分大小写的比较”之类的事情变得很棘手。确实,进行任何形式的区域感知比较都很棘手,而且不区分大小写也很棘手。

虽然有一些限制,但还是有可能完成的。所需的算法可以使用正常的编程实践(和一些静态数据的预计算)“有效地”实现,但它不能像不正确的算法那样有效地实现。通常可以用正确性来换取速度,但结果并不令人愉快。不正确但快速的语言环境实施可能会吸引那些语言环境实施正确的人,但对于那些语言环境产生意外结果的部分观众来说显然是不满意的。

字典顺序不适用于人类

大多数语言环境(“C”语言环境除外)已经以预期的方式处理字母大小写,即仅在考虑所有其他差异后才使用大小写差异。也就是说,如果单词列表按区域设置的排序顺序排序,则列表中仅大小写不同的单词将是连续的。大写单词在小写单词之前还是之后取决于语言环境,但中间不会有其他单词。

该结果不能通过任何单次从左到右逐个字符的比较(“字典顺序”)来实现。而且大多数语言环境都有其他排序规则的怪癖,这些怪癖也不会屈服于幼稚的字典顺序。

如果您有适当的语言环境定义,标准 C++ 排序规则应该能够处理所有这些问题。但它不能简化为仅对 whar_t 对使用比较函数的字典比较,因此 C++ 标准库不提供该接口。

以下只是语言环境感知排序为何复杂的几个示例;在Unicode Technical Standard 10 中可以找到更长的解释以及更多示例。

重音去哪儿了?

大多数浪漫语言(在处理借词时也包括英语)将元音上的重音视为次要特征;也就是说,首先对单词进行排序,就好像不存在重音一样,然后进行第二次遍历,其中非重音字母排在重音字母之前。处理大小写需要第三遍,在前两遍中忽略。

但这不适用于北欧语言。瑞典语、挪威语和丹麦语的字母表有三个额外的元音,在字母表中跟在 z 之后。在瑞典语中,这些元音写成 åäö;在挪威语和丹麦语中,这些字母写成 åæø,而在丹麦语中,å 有时是写成 aa,使奥胡斯成为按字母顺序排列的丹麦城市列表中的最后一个条目。

在德语中,字母 äöü 通常按字母顺序排列,就像带有浪漫口音一样,但在德语电话簿中(有时其他字母列表),它们按字母顺序排列,就好像它们被写成 aeoeue,这是写相同音素的旧风格. (有许多常见的姓氏,例如“Müller”和“Mueller”,发音相同且经常混淆,因此将它们相互对照是有意义的。当我年轻时,加拿大电话簿中的苏格兰名字也使用了类似的惯例;拼写 M'McMac 都聚集在一起,因为它们在语音上都是相同的。)

一个符号,两个字母。或者两个字母,一个符号

德语也有符号ß,它被整理成好像写成ss,尽管它在语音上并不完全相同。稍后我们会再次遇到这个有趣的符号。

事实上,许多语言都将二合字母甚至三合字母视为单个字母。 44个字母的匈牙利字母包括CsDzDzsGyLyNySzTyZs,以及各种重读元音。然而,关于这种现象的文章中最常引用的语言——西班牙语——在 1994 年停止将二合字母 chll 视为字母,大概是因为它更容易强制西班牙裔作家要符合计算机系统而不是改变计算机系统来处理西班牙语有向图。 (维基百科称这是来自“联合国教科文组织和其他国际组织”的压力;每个人都花了很长时间才接受新的字母排序规则,在南美国家的字母列表中,你仍然偶尔会在“哥伦比亚”之后找到“智利”。)

总结:比较字符串需要多遍,有时还需要比较字符组

不区分大小写

由于语言环境在比较中正确处理大小写,因此实际上没有必要进行不区分大小写的排序。进行不区分大小写的等价类检查(“相等”测试)可能很有用,尽管这提出了其他不精确的等价类可能有用的问题。 Unicode 规范化、重音删除,甚至转写为拉丁语在某些情况下都是合理的,而在其他情况下则非常烦人。但事实证明,大小写转换也没有你想象的那么简单。

由于二合字母和三合字母的存在,其中一些具有 Unicode 代码点,Unicode 标准实际上识别三种情况,而不是两种:小写、大写和标题大小写。最后一个是你用来将单词的第一个字母大写的方法,例如,克罗地亚二合字母 dž(U+01C6;单个字符)需要它,它的大写是 DŽ (U+01C4),其标题大小写为 Dž (U+01C5)。 “不区分大小写”比较的理论是,我们可以转换(至少在概念上)任何字符串,使得由“忽略大小写”定义的等价类的所有成员都转换为相同的字节序列。传统上这是通过“大写”字符串来完成的,但事实证明这并不总是可能的,甚至是正确的; Unicode 标准更喜欢使用术语“大小写折叠”,我也是。

C++ 语言环境不能胜任这项工作

所以,回到 C++,可悲的事实是 C++ 语言环境没有足够的信息来进行准确的大小写折叠,因为 C++ 语言环境的工作假设是字符串的大小写折叠仅由顺序和单独组成使用将代码点映射到另一个代码点的函数将字符串中的每个代码点大写。正如我们将看到的那样,这根本行不通,因此它的效率问题是无关紧要的。另一方面,ICU library 有一个接口,它可以像 Unicode 数据库允许的那样正确地进行大小写折叠,并且它的实现是由一些非常好的编码人员精心设计的,因此它可能在约束范围内尽可能高效。所以我绝对推荐使用它。

如果您想全面了解折叠箱的难度,您应该阅读Unicode standard (PDF for chapter 5) 的第 5.18 和 5.19 节。以下只是几个例子。

大小写转换不是从单个字符到单个字符的映射

最简单的例子是德语的ß (U+00DF),它没有大写形式,因为它从不出现在单词的开头,传统的德语正字法也没有使用全部-大写。标准的大写变换是 SS(或在某些情况下是 SZ),但该变换是不可逆的;并非所有 ss 的实例都写为 ß。例如,比较 grüßen 和 küssen(分别表示问候和亲吻)。在 v5.1 中,ẞ 是一个“大写 ß”,作为 U+1E9E 添加到 Unicode 中,但它不常用,除非在全大写街道标志中,法律规定使用它。大写的正常期望大小写 ß 将是两个字母 SS

并非所有的表意文字(可见字符)都是单字符代码

即使大小写转换将单个字符映射到单个字符,它也可能无法将其表示为wchar→wchar 映射。例如,ǰ 可以很容易地大写为 ,但前者是单个组合字形(U+01F0),而第二个是大写 J 和一个组合符号(U+030C)。

ǰ 这样的字形还有一个问题:

简单的逐个字符的大小写折叠可以反规范化

假设我们像上面一样大写ǰ。我们如何大写 ǰ̠ (如果它不能在您的系统上正确呈现,它是同一个字符,下面有一个条,另一个 IPA 约定)?该组合是 U+01F0,U+0320(j 与 caron,组合下面的减号),因此我们继续将 U+01F0 替换为 U+004A,U+030C,然后将 U+0320 保持原样: J̠̌。这很好,但它不会与下面带有正号和减号的规范化大写 J 进行比较,因为在正常形式中,减号变音符号首先出现:U+004A,U+0320,U+030C (J̠̌ ,看起来应该是一样的)。所以有时(很少,说实话,但有时)有必要重新规范化。

抛开 unicode 的怪异,有时大小写转换是上下文相关的

希腊语有很多例子说明标记是如何根据它们是词首、词尾还是词内来打乱的——你可以在 Unicode 标准的第 7 章中阅读更多相关信息——但是简单常见的情况是Σ,它有两个小写版本:σς。具有一定数学背景的非希腊人可能对 σ 很熟悉,但可能不知道它不能用在词尾,必须使用 ς

简而言之

  1. 大小写折叠的最佳可用正确方法是应用 Unicode 大小写折叠算法,该算法需要为每个源字符串创建一个临时字符串。然后,您可以在两个转换后的字符串之间进行简单的按字节比较,以验证原始字符串是否在同一个等价类中。对转换后的字符串进行排序规则排序虽然可能,但效率低于对原始字符串排序的排序规则,并且出于排序目的,未转换的比较可能与转换后的比较一样好或更好。

  2. 理论上,如果您只对大小写相等感兴趣,您可以进行线性转换,记住转换不一定是上下文无关的,也不是简单的字符到字符映射功能。不幸的是,C++ 语言环境没有为您提供执行此操作所需的数据。 Unicode CLDR 更接近,但它是一个复杂的数据结构。

  3. 所有这些东西都非常复杂,并且充满了边缘情况。 (例如,请参阅 Unicode 标准中关于重音立陶宛语 i 的注释。)最好使用维护良好的现有解决方案,其中最好的例子是 ICU。

    李>

【讨论】:

  • 感谢您的反馈(非常感谢),我会考虑您所说的。但是,老实说,它实际上并没有解决我的问题。我正在处理的问题是标准库的纯技术限制,即您必须调用“collat​​e::compare()”并且这样做效率低下。转换为大写然后依赖后一个函数是否 100% 准确是一个完全独立的问题。依赖 3rd-party 库并不总是一个可行的选择,而且对于许多应用程序来说根本没有必要。转换为大写并进行比较通常就足够了。
  • @MikeR:如果您同意转换为大写通常不能通过迭代 wchar_t->wchar_t 映射函数来完成,那么大写的解决方案(或者更好的是,大小写折叠)整个字符串到一个临时缓冲区变得相当合理。在资源受限的环境中(如果这是您的问题),回收临时缓冲区可以显着减少开销。
  • 嗯,这可能就是答案。如果真的别无选择,只能先将整个字符串转换为大写,那么问题就解决了。然而,我的部分困惑源于此链接 (lafstern.org/matt/col2_new.pdf)。作者理论上应该知道他在做什么(鉴于他的凭据),但请快速查看他的“lt_str_1”结构。它是基于字符的,而且嵌套的“lt_char”结构内的返回语句看起来不合法。他应该通过“collat​​e::compare()”路由事情。您是否同意这种解决方案不合法?
  • @MikeR:好的,现在是一篇完整的文章。希望你喜欢。至于链接的文档,那是在 2000 年写的。希望作者从那时起对本地化有了更多的了解。尽管他有程序员资格,但我在他的简历中看不到任何表明他是本地化专家的内容,而且示例中的代码确实无效。我同意“大小写折叠和字节比较”通常足够好(尽管我个人更喜欢大小写折叠和规范化),但它仍然很重要,ICU 比 C++ 标准库做得更好。
  • 好的,勾选(感谢提示)。我同意正确解决这些问题很重要,但也必须考虑成本。考虑到所涉及的所有问题,要“正确”地做到这一点是非常昂贵的。从处理代理对(在我的 UTF-16 Windows 世界中),到尝试在本地 C++ 语言环境的框架内工作(我真的想引入像 ICU 这样的第 3 方库),这很简单,尤其是如果像 Matt Austern 这样的代码实际上会在合理范围内处理大多数情况(在现实世界中)。这是一种权衡,但只是因为“正确”地做可能会非常昂贵。
猜你喜欢
  • 2021-05-03
  • 2021-05-06
  • 1970-01-01
  • 2020-04-18
  • 1970-01-01
  • 2010-10-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多