stl中最难看的组件(没有之一),无疑就是string这货了,一百多个成员函数,当然里面大多数是重载的,不必多想,一个class,如果拥有如此之多的函数,必然一定肯定是失败的,并且,即便是这么一大打函数,string的功能还是很不完备,要不然,就不会有boost里面的string算法。这真是尴尬,string作为最基本最基本的语言组件,又出自官方标准库,长成这样子,真是让无数的c++粉丝要失望,失望归失望,毕竟师出iso,用起来还是很有保障的,论性能什么,再怎样,也不会亏到那里去。只是,很让人好奇的是,这成百个函数又功能不完备的string,里面都有些什么货色,对此,c++exception系列中有过分析。但是,在此,想探讨一下,除了小胡子的方法之外,用其他方法压缩string的成员函数的数量。
我们先来看看string的append成员函数,怪怪龙的东,总共有8个重载之多,好像还不止,突然想起狗语言的名言,少即是多,反过来说,多即是少。
basic_string<CharType, Traits, Allocator>& append(
const value_type* _Ptr
);
basic_string<CharType, Traits, Allocator>& append(
const value_type* _Ptr,
size_type _Count
);
basic_string<CharType, Traits, Allocator>& append(
const basic_string<CharType, Traits, Allocator>& _Str,
size_type _Off,
size_type _Count
);
basic_string<CharType, Traits, Allocator>& append(
const basic_string<CharType, Traits, Allocator>& _Str
);
basic_string<CharType, Traits, Allocator>& append(
size_type _Count,
value_type _Ch
);
template<class InputIterator>
basic_string<CharType, Traits, Allocator>& append(
InputIterator _First,
InputIterator _Last
);
basic_string<CharType, Traits, Allocator>& append(
const_pointer _First,
const_pointer _Last
);
basic_string<CharType, Traits, Allocator>& append(
const_iterator _First,
const_iterator _Last
);
这么多的重载,其实可分为两类,一类是迭代器版本的append,对于插入n个相同的字符append,可以看做是特殊迭代器。另一类是连续字节内存块的append。这里,只关注后一类。虽然有4个之多,但其实只需要一个就行了,那就是 append(const basic_string<CharType, Traits, Allocator>& _Str)。因为字符指针可以隐式转换为string,另外的两个重载可以临时构造string,然后传递进append就好了。之所以存在4个,老朽的猜想可能是因为效率,至于调用上的方便性,并没有带来多少提高。string的其他类似于用append的通过参数来string的操作,如replace,insert,+=,那么多的重载版本,应该也是同样的原因。
假如,临时string对象的构造没有造成任何性能上的损失,那么,应该就可以减少几十个成员函数,这无疑很值得尝试。那么,能否存在廉价的string临时构造方法,因为它知道自己是临时对象,只作为临时参数传递的使命,不会在其上面作什么赋值,添加,修改等操作,也就是说,它是不可变的,那么,这个临时string对象就不需要分配内存了,只要节用ptr作为自己字符串的起始地址,然后以长度作为自己的长度。参数传递使命完成后,也不需要销毁内存了。
可是,C++中,也不仅仅是C++,所有的语言并没有这样的机制来判断对象它在构造的时候,就是仅仅作为参数传递来使用的。为了达到这种目的,很多时候还不惜使用引用计数,但是,很多场合,临时string对象始终要构造缓冲存放字符串,比如这里。
除了C++,任何语言的字符串都是不可变的,任何对于字符串的修改,都意味着要创建另一个全新的字符串来,那怕仅仅是修改了一个字符。其实,不可变的字符串,在C++中运用很广的,很多时候,我们仅仅只需要不可变的字符串,比如说,这里的append,全部只需要immutable的string。只要知道string是immutable的,那么,c++完全可以高效的应付,既然是immutable,就不需要考虑什么资源分配释放的龟毛问题了。下面,就尝试class一个immutable的字符串,这,太容易了。就是:
struct Str
{
typedef const char* PCStr;
PCStr start;
size_t length;
Str(PCStr text, size_t len)
{
start = text;
length = len;
}
//
};
然后,在basic_string中加入operator Str的函数,以完成从一个string到一个Str的隐式转换,这个隐式转换简直没有任何性能上的损失。还有,string中再增加一个Sub的成员函数,用于截取一段子字符串,也即是immutable的Str对象。显然,我们的Str其实表达了一个概念,内存中一节连续的字符内存,也即是数组。
最后,append就变成append(Str str);了。Str加不加const,或者Str是否为引用,关系都不大。下面,看看它的运作。
对于,append(const char* text),由于Str中有一个const char*参数的构造函数,text自动隐式转换为一个Str,很好;
对于,append(const char* text,size_t count),用append(Str(text, count)),就地构造一个临时的Str对象,嗯,语法调用上多了一个Str和一对括号,多了5个字符,的确有点不便。
对于,append(const string& text),同上,string中有一个operator Str的函数,隐式转换自动完成。
对于,append(const string& text,size_t offset,size_t count),用append(text.Sub(offse, count)),就地构造一个临时的Str对象,嗯,语法调用上多了一个Sub和一对括号和一个点,但是少了一个逗号,多了5个字符,有点不便。
即此以推,string中的replace,insert,assign,+=,=等函数,每个平均减少3个,总共差不多可以减少20个左右啦,而功能上没有任何减少,可喜可贺。
然后,string中的各种查找比较操作的const的成员函数,比如find,find_first_not_of,rfind等,都可以挪到Str旗下了。因为这些函数,我们也希望可以用之于其他地方,只要那是一块连续的字符内存好比数组,那么我们就可以就地快速构造一个临时Str对象,进行find,rfind这些操作了。当然,原来string也可以有这个功能,但是想到仅仅为了做一个find或者find_first_not_of的查找,就要分配内存释放内存,对于性能优先的巴普洛夫反应的C++猿猴来说,这绝对是望而生畏的大事。现在通过不可变的Str,马上就释放出来string的成员函数的隐含的生产力了。 由于Str的廉价和透明性,就可以到处乱使用,想用就用,何其快哉。
原来string没有了这些查找的函数,每次要用它们,必须转换这样调用,((Str)text).find,无疑很不方便,对此,我们只要在string中再增加一个Str的成员函数,以返回临时Str对象,就可以text.Str().find(),似乎有点不便,但也不是不能接受。
当然,Str也有缺点,那就是它不以0结束,导致很多对于要求以0结束的地方,就变成禁区了,这坑爹的C语言规定。
这不是很明显吗?字符串的一部分也是字符串,随便取出字符串的一节,本来就应该是字符串,这么简明统一简洁明显的概念,这样可以简化多少代码呢,结果,偏偏只有带有0结束的那一节字符串,才是C语言承认的字符串。一个很好的概念,就这样在很多地方失去用武之地了。你因为以0结束的字符串很好吗,要不cstring头文件中也不会有那么多带有字符串长度版本的字符函数,如strncpy,来补充了。
对了,有没有觉得string中的find_last_of,find_first_of,find_last_not_of,find_first_not_of很碍眼啊,显然这是一种不用组合思想下设计出来的api产物了。其实,别看stl是官方iso的嫡出亲子,但是,内中的很多api的设计都不咋样,实在不是学习的好对象。你还别不服,想想人家C#linq的链式调用,那个用起来,才叫痛快。