GotW#29 不区分大小写的string (Case-Insensitive Strings)
难度:7/10
你期望一个不分大小写的字符串类型吗?你的使命是,应该选个现成的并接受它,还是自己写一个。
问题
写一个不分大小写的字符串类型,它其它方面都与标准库中的“string”类相同,只是在大小写区分上和(非标的,但被广泛使用的)C函数stricmp():
ci_string s( "AbCdE" );
// case insensitive
assert( s == "abcde" );
assert( s == "ABCDE" );
// still case-preserving, of course
assert( strcmp( s.c_str(), "AbCdE" ) == 0 );
assert( strcmp( s.c_str(), "abcde" ) != 0 );
解决方案
写一个不分大小写的字符串类型,它其它方面都与标准库中的“string”类相同,只是在大小写区分上和(非标的,但被广泛使用的)C函数stricmp():
“怎么实现一个不分大小写的字符串类型”这个问题是如此常见,以致于它需要一份专有的FAQ--所以才在GotW中讨论它。
注意1:stricmp()这个不区分大小写的字符串比较函数不是C标准的一部分,但它为很多C编译器扩展提供。
注意2:“不区分大小写”的实际含义完全取决于你的程序和国家语言。例如,很多语言根本就没有大小写;但即使如此,你仍然需要决策重读和非重读字符是否等价,诸如此类。
下面是我们期望达到的目标:本GotW指导了如何为标准string类实现“不区分大小写”,无论你处在什么语境下。
ci_string s( "AbCdE" );
// case insensitive
assert( s == "abcde" );
assert( s == "ABCDE" );
// still case-preserving, of course
assert( strcmp( s.c_str(), "AbCdE" ) == 0 );
assert( strcmp( s.c_str(), "abcde" ) != 0 );
关键点是领会“string类”在标准C++中到底是什么。如果你看一下string的头文件,你将看到如下的东西:
typedef basic_string<char> string;
所以,string并不是一个真正的类,它是一个模板的(特化的)typedef。再向下,basic_string<>模板申明如下,这是其全貌:
template<class charT,
class traits = char_traits<charT>,
class Allocator = allocator<charT> >
class basic_string;
所以,“string”实际上是“basic_string<char, char_traits<char>, allocator<char> >”。我们不必操心分配器(allocator)部分,关键点是char_traits部分,它决定了字符的相互作用和比较运算(!运算)。
basic_string提供了常用的比较函数以比较两个string对象是否相等,或一个小于另一个,等等。这些string类的比较函数是建立在char_traits模板提供的字符比较函数基础上的。具体一点,char_traits模板提供了如下的字符比较函数:eq()(相等)、ne()(不等)、lt()(小于)、compare()(比较字符序列)、find()(搜索字符序列)。
如果你希望在(string的)这些操作上有不同的行为,我们所要做的只是提供一个不同的char_traits模板。这是最容易的方法:
struct ci_char_traits : public char_traits<char>
// 继承为了得到我们不必过载的函数
{
static bool eq( char c1, char c2 )
{ return toupper(c1) == toupper(c2); }
static bool ne( char c1, char c2 )
{ return toupper(c1) != toupper(c2); }
static bool lt( char c1, char c2 )
{ return toupper(c1) < toupper(c2); }
static int compare( const char* s1,
const char* s2,
size_t n ) {
return memicmp( s1, s2, n );
// 如果你的编译器提供了它,
// 不然你就得自己实现一个。
}
static const char*
find( const char* s, int n, char a ) {
while( n-- > 0 && toupper(*s) != toupper(a) ) {
++s;
}
return s;
}
};
最后将它们合在一起:
typedef basic_string<char, ci_char_traits> ci_string;
我们重定义了一个“ci_string”,它的操作非常象标准的“string”,只是它用ci_char_traits代替了char_traits<char>以使用特别的比较规则。我们只不过将ci_char_traits的规则实现为“不区分大小写”,就使得ci_string大动手脚就表现为“不区分大小写”了--也就是说,我们根本没有碰basic_string就有了一个“不区分大小写”的string!
这次的GotW揭示了basic_string模板的工作原理以及实现使用上的灵活性。如果你期望不用上面的memicmp()和toupper()实现的这些比较函数,只需要用你自己的代码替换这5个函数,怎么满足你自己的程序的需求,就怎么实现它们。
习题
1.这样从char_traits<char>继承出ci_char_traits安全吗?为什么安全或为什么不安全?
2. 为什么下面的代码编译不通过?
(WQ注,由于C++的改进,此代码已经可以编译通过,并正常运行!)
ci_string s = "abc";
cout << s << endl;
提示1:参见GotW #19。
提示2:摘自21.3.7.9 [lib.string.io],basic_string的operator<<操作申明如下(是个偏特化):
template<class charT, class traits, class Allocator>
basic_ostream<charT, traits>&
operator<<(basic_ostream<charT, traits>& os,
const basic_string<charT,traits,Allocator>& str);
(WQ注,C++标准库中,现在已将“basic_ostream<charT, traits>& os”改为“ostream&”,所以没问题了。)
ANSWER: Notice first that cout is actually a basic_ostream<char, char_traits<char> >. Then we see the problem: operator<< for basic_string is templated and all, but it's only specified for insertion into a basic_ostream with the same 'char type' and 'traits type' as the string. That is, the standard operator<< will let you output a ci_string to a basic_ostream<char, ci_char_traits>, which isn't what cout is even though ci_char_traits inherits from char_traits<char> in the above solution.
(由于已错误,不译。)
有两个解决办法:定义ci_strings自己的流入/流出函数,或使用“.c_str()”:
cout << s.c_str() << endl;
3. 当在标准string对象和ci_string对象间使用其它操作(如+、+=、=)时,发生什么?例如:
string a = "aaa";
ci_string b = "bbb";
string c = a + b;
答案:还是,定义ci_string自己的这些操作,或使用“.c_str()”:
string c = a + b.c_str();