如何忽略大小写比较字符串 Matt Austern 著 hotman_x
如何忽略大小写比较字符串
Matt Austern 著
hotman_x 译
hotman_x
--2006.02.05
如果你写过用到了字符串的程序(谁没写过?),就有可能遇到过这种情况:视两个仅大小写有差异的字符串为相同。也就是说,你会需要无视大小写的相等性比较、小于比较、子串匹配、排序。而且,说真的,关于标准C++
首先,你可能正在琢磨写一个“大小写无关字符串类”的法子,让我们来看看:是的,这在技术上是多少还是有点可能性的。标准库类型std::string
其实不过是这个模板的别名:std::basic_string<char, std::char_traits<char>, std::allocator<char> >
。它用traits
参数进行所有的比较,即:提供“重新定义好的进行相等性比较、小于比较”的traits
参数,你就可以以此方式来实例化basic_string
,这样一来,<
和==
- 你没法做I/O—— 至少不吃点苦头是做不了。标准库中的I/O 类,如
std::basic_istream
和std::basic_ostream
,也和std::basic_string
一样,是用字符类型及相关traits
来参数化的。(再说一次,std::ostream
仅仅是它的别名:std::basic_ostream<char, char_traits<char> >
。)Traits
参数必须完全匹配。如果你的字符串用的是std::basic_string<char, my_traits_class>
,那么你的流输出就得相应的用std::basic_ostream<char, my_traits_class>
,这么一来,你就不能用cin
、cout
- “大小写无关”这个特性其实与对象无涉,而是与“如何使用对象”相关。你可能非常需要在此处不关心大小写而在彼处关心(比如说“大小写无关”是一个用户控制的选项)。为这两种情况搞出两个单独的类型出来,实在是在这两者之间不必要的人为设限。
- 不大恰当。同其它
traits
类1 一样,char_traits
- 不充分。即使所有
basic_string
的成员函数都大小写无关了,当你需要用一个非成员的泛型算法(如std::search
和std::find_end
)时还是用不上劲。如果为了提高效率,你决定将容纳basic_string
更好的解决之道,也是更符合标准库习惯的解决之道,应该是:在需要时明确的要求进行大小写比较。象string::find_first_of
和string::rfind
std::sort(C.begin(), C.end(), compare_without_case);
文章的剩余部分我们就来讨论如何写出这个函数对象。
第一次试验
排列单词的方法非止一途。下次你去书店的时候,注意一下作者的名字是怎么安排的:Mary McCarthy 是在Bernard Malamud
逐字母序比较可能不适用于特定的应用(没有那个法子能够适用于各种特定应用;对于人名和地名来说,一个专门的库可能更合适些),不过适合很多情况,而且这是C++ 中默认的“字符串比较”的含义。字符串是字符的序列,且若x 和y 是std::string
类型的对象,则表达式x<y
等效于表达式:std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end())
在这个表达式中,lexicographical_compare
用operator<
比较单个字符,但有个lexicographical_compare
版本允许你选择比较字符的方法。这个版本的lexicographical_compare
有五个参数,多出来的那个参数是一个函数对象——一个用来判定哪个字母排在另一个前面的Binary Predicate2 。为了使用lexicographical_compare
对字符做大小写无关比较,通常的想法就是把两个字符都变成大写的,再来比较。利用众所周知的标准C
struct lt_nocase : public std::binary_function<char, char, bool> {
bool operator()(char x, char y) const {
return toupper(static_cast<unsigned char>(x)) < toupper(static_cast<unsigned char>(y));
}
};
“所有复杂的问题都存在这样一个解:它简单、整洁,而且错误。”写C++
这里有个例子,能让你看出问题之所在:
int main()
{
const char* s1 = "GEW/334RZTRAMINER";
const char* s2 = "gew/374rztraminer";
printf("s1 = %s, s2 = %s/n", s1, s2);
printf(
"s1 < s2: %s/n",
std::lexicographical_compare(
s1, s1 + 14, s2, s2 + 14, lt_nocase()
) ? "true" : "false"
);
}
你可以在你的系统上试一试。在我的系统上(一台运行IRIX 6.5 的silicon Graphics O2
s1 = GEWÜRZTRAMINER, s2 = gewürztraminer
s1 < s2: true
"gewürztraminer
和"
"GEWÜRZTRAMINER"
难道不是一样的吗?现在做一点小小的改动:如果你插入这么一行setlocale(LC_ALL, "de")
; 在printf
s1 = GEWÜRZTRAMINER, s2 = gewürztraminer
s1 < s2: false
大小写无关字符串比较远比它表面上看到的要复杂。这个看起来如此单纯的程序严重的依赖着一个我们总是忽略的东西:locale
Locales
一个字符无非是一个小整数而已。我们可以选择把一个小整数解释为一个字符,但这种解释并无统一的法则。一个特定的数字是被解释为一个字母?一个标点?抑或是一个不可见的控制字符?这个问题并不存在唯一正确答案,and it doesn't even make a difference as far as the core C and C++ languages are concerned
例如,isalpha
,用来决定一个字符是否字母;toupper
默认情况下,字符操作函数在“处理简单英语文本”的字符集下工作。字符'/374'
不受toupper
的影响是因为它不是一个字母;在某些系统上打印出来时,它看起来是个ü
,但对于处理英语文本的C 库函数来说毫不相关——在ASCII 字符集中压根就没有ü
setlocale(LC_ALL, “de”);
告诉C 库,下面开始按照德语习惯进行处理(至少在IRIX 系统上是这样。Locale 名称没有标准化)。德语中有字母ü ,于是toupper
将ü
转换为Ü
如果你还没有警惕,这可是时候了。虽然toupper
可能看起来不过是一个仅有一个参数的简单函数,其实它依赖一个全局变量——更有甚者,这是一个隐藏的全局变量。这导致了所有常见的难题:一个调用了toupper
如果你用toupper
来做大小写无关的字符串比较,这就会引起灾难。如果你使用了一个依赖于“已排好序的线性表”的算法(如二叉搜索),而一个新的locale 却悄悄的引起排序变化,想想会出现什么事?象这样的代码是不可复用的,亦不堪大用。你不能在“在多种情况下被引用”的库代码中用它,哪怕是在决不调用setlocale
的程序中也不行。你可能会转头又在一个大的程序中使用这些代码,但是你将会遇到一个维护问题:也许你能证明决没有其它模块调用过setlocale
,但你能证明明年这个程序的版本也没有其它模块调用setlocale
在c 中,这个问题没有好的解法。因为C 库只有一个单一的全局locale ,就是这样。但是C++
C++ 中的 Locales
在C++ 标准库中,locale 不在是一个深藏于库实现代码中的全局数据。它是一个std::locale
类型的对象,而且你可以创建它,并象其它对象那样把它传递给函数。你可以创建一个locale 对象来代表常用的locale
std::locale L = std::locale::classic();
或者你可以创建一个德语locale
std::locale L("de");
( 由于在 C 库中, locale 的名字没有标准化。查看你的库实现文档来找出可用的 locale 名称。 )
C++ 中的 locale 被分为多个截面( facet ),每个不同截面处理一个国际化的不同方面,而函数 std::use_facet
3 从 locale 对象中提取特定载面。其中 ctype
截面处理字符分类,包括大小写转换。
最后,如果c1 和c2 是char
类型,这个代码片段将会按照指定的locale L
const std::ctype<char>& ct = std::use_facet<std::ctype<char> >(L);
bool result = ct.toupper(c1) < ct.toupper(c2);
有一个特定的缩写方法,你可以这么写:
std::toupper(c, L);
这(如果c 是char
std::use_facet<std::ctype<char> >(L).toupper(c) ;
这可以有效的减少use_facet
题外话:另一个facet
如果你已经熟悉C++ 的locale ,你可能已经想到了另一个比较字符串的方法:collate
截面正是为“封装排序的细节”而设,它有一个接口类似C 库函数strcmp
的成员函数。甚至还有一个小小的方便:如果L 是一个locale
对象,你可以写L(x, y)
来比较两个字符串,省去调用use_facet
再调用collate
“经典”的locale
有一个collate
截面,专门来做逐字母比较,就象string
的<
操作符一样,只不过(与string
的<
操作符不同的是,)无论进行哪种比较,用的是其它的locale
。如果你的系统恰好有进行大小写无关比较所需的locale
不幸的是,这个搞法虽然正确,对于那些没有这种系统的人没什么帮助。可能有一天一组这样的locale
大小写无关的字符串比较
使用ctype
,通过大小写无关的字符比较还建立大小写无关的字符串比较是相当直接的。这个版本没有优化,不过至少它是正确的,象上文一样,它本质上使用了正确的技术:用lexicographical_compare
比较两个字符串,而比较两个字符则是先将它们都转为大写。不过这一次,我们小心的使用locale 对象取代了全局变量。(作为一个副作用,“将两个字符转为大写”并不能保证与“将两个字符转为小写”给出相同的结果:因为并不能保证转换的结果是可逆的。例如,在法语中,习惯上大写状态是忽略重音标记的。这样,在法语中toupper
就顺理成章的变成了一个有损变换:它可能将'é' 和'e' 都变换成同一个大写字母'E' 。在这样的locale 中,“使用toupper
的大小写无关的比较”会说'é' 和'E' 是一码事,而“使用tolower
struct lt_str_1
: public std::binary_function<std::string, std::string, bool>
{
struct lt_char {
const std::ctype<char>& ct;
lt_char(const std::ctype<char>& c) : ct(c) {}
bool operator()(char x, char y) const {
return ct.toupper(x) < ct.toupper(y);
}
};
std::locale loc;
const std::ctype<char>& ct;
lt_str_1(const std::locale& L = std::locale::classic())
: loc(L), ct(std::use_facet<std::ctype<char> >(loc)) {}
bool operator()(
const std::string& x, const std::string& y
) const {
return std::lexicographical_compare(
x.begin(), x.end(), y.begin(), y.end(), lt_char(ct)
);
}
};
这还没怎么优化,比应有的速度要慢。这个问题是技术性的,令人恼火:我们在循环中调用toupper
,而C++ 标准要求toupper
进行一次虚拟函数调用。可能有些优化器聪明得很,会将虚拟函数调用的负担移到循还外边去,不过大部分优化器不会这么聪明。在循环中,虚拟函数调用的开销应当尽量避免。在本例中,做到这一点的方法不是那么直接。这诱导我们想到:正确答案应该是ctype
const char* ctype<char>::toupper(char* f, char* l) const ,
这个函数转换区间[f, l)
一个替代方案是:为每个字符做一次转换,缓存结果。这不是一个通用的解决法子,它有可能完全没法运转——比如说你用32 位UCS-4 字符。如果你用char
(在大多数系统上是8 位),那么,在比较函数对象里维护一个256
struct lt_str_2
: public std::binary_function<std::string, std::string, bool>
{
struct lt_char {
const char* tab;
lt_char(const char* t) : tab(t) { }
bool operator()(char x, char y) const {
return tab[x - CHAR_MIN] < tab[y – CHAR_MIN];
}
};
char tab[CHAR_MAX - CHAR_MIN + 1];
lt_str_2(const std::locale& L = std::locale::classic()) {
const std::ctype<char>& ct
= std::use_facet<std::ctype<char> >(L)
;
for (int i = CHAR_MIN; i <= CHAR_MAX; ++i)
tab[i - CHAR_MIN] = (char) i;
ct.toupper(tab, tab + (CHAR_MAX - CHAR_MIN + 1));
}
bool operator()(
const std::string& x, const std::string& y
) const {
return std::lexicographical_compare(
x.begin(), x.end(), y.begin(), y.end(), lt_char(tab)
);
}
};
如你所见,lt_str_1
和lt_str_2
完全不一样。前者有一个直接用ctype
的字符比较函数对象,而后者的字符比较函数对象则用了一个预先计算的大写转换表。如果创建一个lt_str_2
函数对象,用来比较一些短字符串,然后扔掉它,那么可能会慢一些。不过,对于连续使用来说,lt_str_2
会明显的比lt_str_1
要快。在我的系统上,这个差别超过两倍:lt_str_1
排序23791 个单词用的0.86 秒,而lt_str_2
仅用0.4
从本文我们学到了什么?
- 大小写无关的字符串类是一种错误的抽象。C++ 标准库中的算法,是以policy
- 逐字母进行字符串比较建立在字符比较的基础之上。一旦你有了一个大小写无关的字符比较函数对象,这个问题就解决了。(而且,如果你可能重用这个函数对象来比较其它类型的字符序列,如vector<char> ,或者字符串表,如普通的C
- 大小写无关的字符比较比想象的要困难,如果脱离了特定的locale
- 正确的大小写无关比较用到了很多机制,不过你只需要写一次。你可能不想考虑locale—— 大多数人都不想(90 年代的时候谁乐意考虑千年虫问题?),但要想清楚,相 比于草草的处理locale 依赖问题,如果你把依赖于locale 的代码写对了,你更有可能跳脱于locale
1 参见 Andrei Alexandrescu 在四月号上的文章
2 hotman_x 曰:很想将Binary Predicate
3 警告:use_facet 是一个函数模板,它的模板参数只出现在返回值中,而不出现在任何参数中。要调用它须使用一个叫“显式模板参数特化”的语言特性,一些C++ 编译器还不支持这个特性。如果你在用的编译器不支持它,你的库实现者可能会提供一个折衷的法子让你可以用其它方式调用use_facet