一简介
字符串分词,即按照某一规则,将一个完整的字符串分割为更多的字段。在 C库当中, strtok/wcstok提供了类似的功能, C++标准库兼容了 C库。 C++的 stringstream有类似的功能, boost.string_algorithm也有提供类似的泛型算法。另外在 boost当中专门提供了 boost.tokenizer来做这样的工作,它的实现是对 C++泛型设计的一个不错的诠释,当然,它远没有达到完美的程度。 Matthew Wilson在它的 stlsoft中也提供了类似的组件, stlsoft.string_tokeniser。它们各有各自的特点,接下来我们对此做一些探讨和研究。二 C库
C库中提供了 strtok/wcstok来实现类似的功能,但是它们具有明显的缺点:- 不可重入性。这是因为它用内部的静态变量来保存相关状态。如果C库实现没有考虑TLS的话,则还有竞争条件的问题(更多信息可以参考<Windows via C/C++, Fifth Edition> Chapter 21: Thread-Local Storage)。
- 参数必须是可写入的。
- 参数必须是C风格字符串。
- 总是跳过空白。
#include <iostream>
using namespace std;
int main()
{
char str[] = "abc,def;ghi,jkl;;";
char* outer = NULL;
char* inner = NULL;
for( outer = strtok( str, ";") ; NULL != outer; outer = strtok(NULL, ";") )
{
printf( "Outer token: %s\n", outer );
//for( inner = strtok( outer, ","); NULL != inner; inner = strtok( NULL, ",") )
//{
// printf( "Inner token: %s\n", inner );
//}
}
return 0;
} 如上面的程序,如果解注释那一段代码将导致工作不正常,也不会达到我们想要的目的,输出可能如下 : Outer token: abc,def Inner token: abc Inner token: def 请按任意键继续 . . . 在 Windows下面, Visual C++ 2005起提供了新的安全版函数,在一定程度上可以解决这种问题 (UNIX系统下面有 strtok_r有类似的功能 ),因为它们是可重入的。下面是上面例程的升级版:#include <iostream>
using namespace std;
int main()
{
char str[] = "abc,def;ghi,jkl;;";
char* outer = NULL;
char* inner = NULL;
char* pOut = NULL;
char* pIn = NULL;
for( outer = strtok_s( str, ";", &pOut ) ; NULL != outer; outer = strtok_s(NULL, ";", &pOut) )
{
printf_s( "Outer token: %s\n", outer );
for( inner = strtok_s( outer, ",", &pIn); NULL != inner; inner = strtok_s( NULL, ",", &pIn ) )
{
printf_s( "Inner token: %s\n", inner );
}
}
return 0;
} 在我的机器上输出如下: szRes:<abc> || pTok: <abc> szRes:<abc> || pTok: <def> szRes:<abc> || pTok: <g> szRes:<abc> || pTok: <efg> 请按任意键继续 . . . 但是即便是如此,我们也不能解决它所存在的其它问题,比如刚才提到的 2、 3、 4点。三 C++ stringstream
这也是一种可以用来分词的方法,但是实际上用的并不多,而且功能也不够强大,而且很多人都不能很好的掌握 stringstream,因为我们平时用得太少了。#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
stringstream str("abcd efg kk dd " );
string tok;
while( getline( str, tok, ' ' ) )
{
cout<<tok << endl;
}
return 0;
} 输出如下: abcd efg kk dd 请按任意键继续 . . .四 boost字符串算法库
在我写的《 C++ String深入详解 2.0》中对它做过一些介绍,但是那还不够,在未来的 3.0版本中将会涵盖更多的相关内容。字符串算法库中也有提供字符串分词的泛型算法和迭代器。4.1 泛型算法
这种方式基于 Range的概念,需要我们提供一个可以容纳被拆分后字符串的容器,下面是它的一个简单例程:#include <iostream>
#include <vector>
#include <string>
using namespace std;
#include <boost/algorithm/string.hpp>
int main()
{
string ss( "HelloWorld!He.lloWorld!he" );
vector<string> tmp;
// 以标点符号分开!
vector<string>& tt = boost::algorithm::split( tmp, ss, boost::algorithm::is_punct() );
assert( boost::addressof(tmp) == boost::addressof(tt) );
copy( tt.begin(),tt.end(),ostream_iterator< string >( cout,"\n" ) );
return 0;
} 输出: HelloWorld He lloWorld he 请按任意键继续 . . . 我们可以看到,我们对整个拆分过程是不可控的,即使在某些时候我们可能只对拆分后的前两个单词感兴趣我们也不得不用容器来获取和保存所有结果,这对于字符串很长的情况那实在是让人不能接受,或许我们应该“按需分配”,所以便有了迭代器的方法。4.2 迭代器
boost::algorithm::split_iterator可以用来拆分字符串,同时还需要搭配一些 Finder(比如 token_finder)和断言式(或者说判断式)。当然我们也可以自己 DIY一个 Finder。下面是一个简单的例程:#include <iostream>
#include <string>
using namespace std;
#include <boost/algorithm/string.hpp>
int main()
{
string str("abc@ d*dd a");
boost::algorithm::split_iterator< string::iterator > iStr(
str,
boost::algorithm::token_finder( boost::algorithm::is_any_of( "@* " ) )
);
boost::algorithm::split_iterator< string::iterator> end;
while( iStr != end )
{
cout<< *iStr << endl;
++iStr;
}
return 0;
} 输出: abc d dd a 请按任意键继续 . . . 这个输出可能不是我们想要的,将代码做一点点修改:boost::algorithm::split_iterator< string::iterator > iStr(
str,
boost::algorithm::token_finder(
boost::algorithm::is_any_of( "@* " ),
boost::algorithm::token_compress_on )
); 这个时候将开启压缩,输出可能如下: abc d dd a 请按任意键继续 . . . 相对于 boost.tokenizer,字符串算法库提供的分词手法要少一些,如果要更多的功能的话我们还是需要自己 DIY一个 Finder的。自己 DIY一个 Finder并不复杂,我们只需要保证我们的 Finder拥有与此类似的重载即可:template< typename ForwardIteratorT >
iterator_range<ForwardIteratorT>
operator()(
ForwardIteratorT Begin,
ForwardIteratorT End ) const; 至于这个 Finder内部您要保存什么信息都可以由您自己决定。这和 boost.tokenizer采用的策略也是类似的,因此它们两个的扩展性都是很强的。本来应该多说一些关于 Boost字符串算法库的内容的,因为毕竟 Tr2中有它,但是这不是这个文档的重点。五 boost.tokenizer
boost.tokenizer是一个专门提供的字符串分词库,它本身由视图容器和一些迭代器以及迭代器视图组成。虽然我认为可能随着 Boost字符串算法库的日趋成熟和强大,这个库可能会被拿掉,但是研究和学习它的一些东西还是有一些价值的。5.1 组件
5.1.1 tokenizer tokenizer是一个视图容器,它本身并不包含具体的数据,它存在于 boost\tokenizer.hpp中。template <
typename TokenizerFunc = char_delimiters_separator<char>,
typename Iterator = std::string::const_iterator,
typename Type = std::string
>
class tokenizer TokenizerFunc : 一个符合 TokenizerFunc概念的拆分工具。 Iterator : 用于访问每个拆分后数据的迭代器。 Type: 用于保存拆分后的数据。 5.1.2 token_iterator token_iterator是一个迭代器,它用于访问我们的拆分后数据,它位于 boost\token_iterator.hpp中。template <class TokenizerFunc, class Iterator, class Type>
class token_iterator
: public iterator_facade<
token_iterator<TokenizerFunc, Iterator, Type>
, Type
, typename detail::minimum_category<
forward_traversal_tag
, typename iterator_traversal<Iterator>::type
>::type
, const Type&
> 这是它的声明,如果您不了解新式迭代器的概念以及模板元编程,可能理解这段代码有一些困难,但是这并不重要。我简单告诉您的就是从 iterator_façade派生可以很轻松的得到一些迭代器的行为,同时只需要派生类实现一些必要的成员函数以符合其概念即可。后面的一个模板元过程只是保证我们的迭代器最多只能是前向迭代器,即使我们使用一个随机迭代器来具现化 token_iterator也会被当做前向迭代器。通常从 iterator_façade派生都需要将类iterator_core_access设置为友元类。 迭代器token_iterator保存了如下数据成员:TokenizerFunc f_;
Iterator begin_;
Iterator end_;
bool valid_;
Type tok_;
它们分别是:分词工具类对象、开始位置、结束位置、有效位以及结果。 在 token_iterator的实现中,这两个函数很重要:void increment(){
BOOST_ASSERT(valid_);
valid_ = f_(begin_,end_,tok_);
}
const Type& dereference() const {
BOOST_ASSERT(valid_);
return tok_;
} 他们分别对应了迭代器自增和提领操作。因此我们可以知道提领操作返回的只是一个常引用,并不会有什么太大的开销,而对于自增来说,它的开销取决于拆分工具的实现。更直接的来说就是取决于拆分工具的 operator ()的实现。 5.1.3 分词工具类 (TokenizerFunc) 的概念模型 boost.tokenizer为我们提供了四个内置的工具类,它们分别是: char_separator、 escaped_list_separator、 offset_separator以及 char_delimiters_separator。其中 char_delimiters_separator已经被 deprecated了,我们应该使用 char_separator来代替它。它们的实现都位于 boost\token_functions.hpp中。 在详细介绍这种工具类之前必须描述一下它的模型和概念,因为如果我们要自己 DIY一个分词工具类的话,那么就需要符合它的规则。 首先TokenizerFunc在应用中被tokenizer和token_iterator按值保存,因此它应该是可拷贝构造的,参考源码我们可以发现类似的代码:void assign(Iterator first, Iterator last, const TokenizerFunc& f){
assign(first,last);
f_ = f;
} 因此, TokenizerFunc应该是可赋值的。由于不存在友元关系,因此这两个函数应该必须是 public的。 参考 tokenizer和 token_iterator的实现还可以发现用到它的其它地方,下面是一个摘录:void initialize(){
if(valid_) return;
f_.reset();
valid_ = (begin_ != end_)?
f_(begin_,end_,tok_):false;
} 因此我们可以推断出 TokenizerFunc应该具有一个 reset的成员函数,它的意义应该是保证迭代器可以用于一个新的分析得以进行。另外还应该具有一个 operator ()的重载,这个重载应该具有三个或者更多的参数,并且支持 3个参数的调用(其它的参数有默认值)。这至少的三个参数是开始位置、结束位置以及保存分析结果的 tok_,这里的 tok_应该是作为引用传递的。并且这个 operator()总是有一个 bool返回值,如果返回 true则代表分析可以继续;如果返回 false则对迭代器的有效性产生影响。5.2 工具类解析
5.2.1 char_separator char_separator可能是我们最常用到的工具了,让我们先学会如何使用它。例子 1(摘自 boost文档 ):#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include <boost/tokenizer.hpp>
int main()
{
std::string str = ";;Hello|world||-foo--bar;yow;baz|";
typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
boost::char_separator<char> sep("-;|");
tokenizer tokens(str, sep);
for (tokenizer::iterator tok_iter = tokens.begin();
tok_iter != tokens.end();
++tok_iter)
{
std::cout << "<" << *tok_iter << "> ";
}
std::cout << "\n";
return 0;
} 输出: <Hello> <world> <foo> <bar> <yow> <baz> 请按任意键继续 . . . 它除了分隔符之外其它全部使用默认的参数,这将使得分词过程将遗弃所有的 sep参数中的字符。但是 char_separator并不仅仅是作为 strtok的替代物存在的,它比 strtok强大得多。 下面是 char_separator的数据成员:private:
string_type m_kept_delims;
string_type m_dropped_delims;
bool m_use_ispunct;
bool m_use_isspace;
empty_token_policy m_empty_tokens;
bool m_output_done; char_separator多了这样的一些概念:遗弃分隔符、保留分隔符以及 empty开关。 遗弃分隔符:我们可以简单的认为它就是不会出现在分割后的任何一个结果里面。 保留分隔符:任何一个保留分隔符都将作为一个独立的结果存在。 empty开关:处理是否将empty视为一个结果。 将刚才的代码做一点点简单的修改: boost::char_separator<char> sep("-;", "|"); 其它不变,我们将得到输出: <Hello> <|> <world> <|> <|> <foo> <bar> <yow> <baz> <|> 请按任意键继续 . . . 另外如果将输出字符串修改为这样: std::string str = ";;Hello|wor ld||-foo--bar;yo w;baz|"; 此时将得到输出: <Hello> <|> <wor ld> <|> <|> <foo> <bar> <yo w> <baz> <|> 请按任意键继续 . . . 我们可以看到空格并没有被视为一个分隔符。要想将空格视为分隔符需要在 sep的第一个参数中显式的指定,比如: boost::char_separator<char> sep("-; ", "|" ); 现在来看保留 empty的情况,代码如下:#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include <boost/tokenizer.hpp>
int main()
{
std::string str = ";;Hello|wor ld||-foo--bar;yo w;baz|";
typedef boost::tokenizer<boost::char_separator<char> > tokenizer;
boost::char_separator<char> sep("-;", "|", boost::keep_empty_tokens );
tokenizer tokens(str, sep);
for (tokenizer::iterator tok_iter = tokens.begin();
tok_iter != tokens.end();
++tok_iter)
{
std::cout << "<" << *tok_iter << "> ";
}
std::cout << "\n";
return 0;
} 结果: <> <> <Hello> <|> <wor ld> <|> <> <|> <> <foo> <> <bar> <yo w> <baz> <|> <> 请按任意键继续 . . . 其实,在绝大多数时候,我们都只需要使用默认的参数即可:explicit
char_separator(const Char* dropped_delims,
const Char* kept_delims = 0,
empty_token_policy empty_tokens = drop_empty_tokens) 下面是一个简单例程(选自 boost文档):#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include <boost/tokenizer.hpp>
int main()
{
std::string str = "This is, a test";
typedef boost::tokenizer<boost::char_separator<char> > Tok;
boost::char_separator<char> sep; // 缺省构造
Tok tok(str, sep);
for(Tok::iterator tok_iter = tok.begin(); tok_iter != tok.end(); ++tok_iter)
std::cout << "<" << *tok_iter << "> ";
std::cout << "\n";
return 0;
} 输出: <This> <is> <,> <a> <test> 请按任意键继续 . . . 5.2.2 escaped_list_separator 这个组件用于分析和提取 csv格式的字符串。关于 csv: http://baike.baidu.com/view/468993.htm 下面是一个简单例程 (修改自 boost文档 ):#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include <boost/tokenizer.hpp>
using namespace boost;
int main()
{
try
{
string s = "Field 1,\"putting quotes around fields, allows commas\",Field 3";
tokenizer<escaped_list_separator<char> > tok(s);
for(tokenizer<escaped_list_separator<char> >::iterator beg=tok.begin(); beg!=tok.end();++beg)
{
cout << *beg << "\n";
}
}
catch( boost::escaped_list_error& e )
{
cerr<< e.what() << endl;
}
return 0;
} 输出: Field 1 putting quotes around fields, allows commas Field 3 请按任意键继续 . . . 我建议您不要用它,因为它有很多问题,这个稍后再说。 5.2.3 offset_separator 这个工具类很简单,它的构造如下:template <typename Iter>
offset_separator(Iter begin, Iter end, bool wrap_offsets = true,
bool return_partial_last = true)
: offsets_(begin,end), current_offset_(0),
wrap_offsets_(wrap_offsets),
return_partial_last_(return_partial_last) { }
offset_separator()
: offsets_(1,1), current_offset_(),
wrap_offsets_(true), return_partial_last_(true) { } 重要的概念有两个 (选自 boost文档 ): wrap_offsets_: 指明当所有偏移量用完后是否回绕到偏移量序列的开头继续。例如字符串 "1225200101012002" 用偏移量 (2,2,4) 分解,如果 wrap_offsets_ 为 true, 则分解为 12 25 2001 01 01 2002. 如果 wrap_offsets_为 false, 则分解为 12 25 2001,然后就由于偏移量用完而结束。 return_partial_last_: 指明当被分解序列在生成当前偏移量所需的字符数之前结束,是否创建一个单词,或是忽略它。例如字符串 "122501" 用偏移量 (2,2,4) 分解,如果 return_partial_last_ 为 true,则分解为 12 25 01. 如果为 false, 则分解为 12 25,然后就由于序列中只剩下2个字符不足4个而结束。 简单的例程(选自 boost文档):#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include <boost/tokenizer.hpp>
using namespace boost;
int main()
{
string s = "12252001";
int offsets[] = {2,2,4};
offset_separator f(offsets, offsets+3);
tokenizer<offset_separator> tok(s,f);
for(
tokenizer<offset_separator>::iterator beg=tok.begin();
beg!=tok.end();
++beg
)
{
cout << *beg << "\n";
}
return 0;
} 输出: 12 25 2001 请按任意键继续 . . .5.3 boost.tokenizer的缺陷
虽然 boost.tokenizer是本文档的重点,但是我对于它的态度是这样的:认真的学习它,领会它好的地方,学会它的设计思路和方法,找出它的缺陷,然后永远不要用它或者自己优化之后再用它。 5.3.1 效率问题 参考escaped_list_separator的实现可以发现,对字符串的查找动作使用的是标准库泛型算法find_if。bool is_escape(Char e) {
char_eq f(e);
return std::find_if(escape_.begin(),escape_.end(),f)!=escape_.end();
}
bool is_c(Char e) {
char_eq f(e);
return std::find_if(c_.begin(),c_.end(),f)!=c_.end();
}
bool is_quote(Char e) {
char_eq f(e);
return std::find_if(quote_.begin(),quote_.end(),f)!=quote_.end();
} 如上所示,很明显,这种搜索方法肯定不如字符串自带的 find函数,实际上在这里 find_if没有必要。参考 Visual c++ 2008 sp1的 STL中 find_if的实现:template<class _InIt,
class _Pr> inline
_InIt _Find_if(_InIt _First, _InIt _Last, _Pr _Pred)
{ // find first satisfying _Pred
_DEBUG_RANGE(_First, _Last);
_DEBUG_POINTER(_Pred);
for (; _First != _Last; ++_First)
if (_Pred(*_First))
break;
return (_First);
}
template<class _InIt,
class _Pr> inline
_InIt find_if(_InIt _First, _InIt _Last, _Pr _Pred)
{ // find first satisfying _Pred
_ASSIGN_FROM_BASE(_First,
_Find_if(_CHECKED_BASE(_First), _CHECKED_BASE(_Last), _Pred));
return (_First);
}
这并没有对字符串搜索做任何优化,同时也无从优化。另外 find函数对字符串搜索使用::memchr进行优化。而 basic_string的成员函数 find使用的是 char_traits::find,而这个 find也是使用::memchr来进行了优化的,对于宽字符串来说使用::wmemchr进行优化。除了搜索算法不合理之外还有重复计算的问题。参考char_separator的实现中,在
template <typename InputIterator, typename Token>
bool operator()(InputIterator& next, InputIterator end, Token& tok) 中有这样的代码:else
{
if (is_dropped(*next))
{
start=++next;
}
for (; next != end && !is_dropped(*next) && !is_kept(*next); ++next)
{
assigner::plus_equal(tok,*next);
}
m_output_done = true;
} 很明显,is_dropped很有可能会遭遇重复计算的问题,或许is_dropped效率很高,但是我想再快也比不过访问一个bool变量吧?这样的行为在其它地方也可以看到。 5.3.2 字符集问题 支持多字符集是一个库是否强大的标志之一,因为标准库的 basic_string提供了 char和 wchar_t的实现,那么我们的字符串分词也应该至少支持这两种字符,然而实际上我们发现 boost.tokenizer做不到。 比如下面的代码:explicit escaped_list_separator(Char e = '\\',
Char c = ',',Char q = '\"')
: escape_(1,e), c_(1,c), quote_(1,q), last_(false) { } 比如当使用 wchar_t来具现化的时候,这能通过编译吗?不能。 再次参考 char_separator的两个私有函数:bool is_kept(Char E) const
{
if (m_kept_delims.length())
return m_kept_delims.find(E) != string_type::npos;
else if (m_use_ispunct) {
return std::ispunct(E) != 0;
} else
return false;
}
bool is_dropped(Char E) const
{
if (m_dropped_delims.length())
return m_dropped_delims.find(E) != string_type::npos;
else if (m_use_isspace) {
return std::isspace(E) != 0;
} else
return false;
} 注意上面红色的部分,类似这样的操作是不能够写死的,这样无法支持 wchar_t,这应该通过一个 policy或者 traits来实现。因此 boost.tokenizer是不能很好的支持宽字符集的,至少库为我们提供的工具类不能很好的支持。相对而言, stlsoft在这方面处理的就好得多,当然我们也可以通过自己 DIY一个 Finder来实现更广泛的字符集支持和更具效率的字符串分词。六 stlsoft::string_tokeniser
stlsoft是 Matthew Wilson所做的一个程序库,它的网站是: http://www.stlsoft.org/ 我们可以免费得到它,而且它的实现全部位于头文件中,无需编译。在 Matthew Wilson的书《 Extended STL, Volume 1 : Collections and Iterators》中对这个字符串分词做了一些介绍,相对而言它是一个很不错的实现。其实现位于 stlsoft\string\string_tokeniser.hpp中。 在运行下面的程序之前,请确保已经安装和配置好了 stlsoft。#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
#include <stlsoft/string/string_tokeniser.hpp>
int main()
{
const wstring strRes(L":abc::def:ghi:jkl::?kk?dd::::::::");
stlsoft::string_tokeniser< wstring, wchar_t > tokens( strRes, L':' );
wcout.imbue( locale("chs") );
copy(
tokens.begin(),
tokens.end(),
ostream_iterator< wstring, wstring::value_type >( wcout, L"\n" ) );
return 0;
}; 输出: abc def ghi jkl ?kk?dd 请按任意键继续 . . .七效率大PK
现 在让我们来对上面所提到的一些东东进行测试吧,首先说明一下这个测试是不完整的,并不代表它们每一个在各种不同情况下的表现。设计本来就是一个相互取舍的 过程,也许它在这种情况下表现不好,但是到了另外一种情况下它反而是最好的选择,因此,不能以这样的一个简单的测试来彻底肯定或者彻底的否定它们中的任何 一个。在这里我们忽略vector可能的内存重分配对效率的影响。 源码如下:#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <algorithm>
using namespace std;
#define __HAVE_STL_SOFT__ // 如果没有安装stlsoft请注释这一行即可
#ifdef __HAVE_STL_SOFT__
#include <stlsoft/string/string_tokeniser.hpp>
#endif // #ifdef __HAVE_STL_SOFT__
#include <boost/tokenizer.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/progress.hpp>
using namespace boost;
int main()
{
const string str( "abc*def*eght*kkk*ddd" );
const int ciCount = 20000;
typedef std::vector< string> STR_VEC;
STR_VEC vec;
vec.reserve( 1000 );
{
cout<< "C-strtok_s: ";
char sz[100];
boost::progress_timer timer;
int i = 0;
while( i++ < ciCount )
{
vec.clear();
char* pTok = NULL;
char* pContext = NULL;
pTok = strtok_s( sz, "*", &pContext );
while( pTok != NULL )
{
vec.push_back( pTok );
pTok = strtok_s( NULL, "*", &pContext );
}
strcpy_s( sz, str.c_str() );
}
}
vec.clear();
{
cout<<"boost.string_algorithm_container: ";
boost::progress_timer timer;
int i=0;
while ( i++ < ciCount )
{
vec.clear();
vec = boost::algorithm::split( vec, str, bind2nd( equal_to<char>(), '*' ) );
}
}
vec.resize(10); // 防止崩溃
{
cout<<"boost.string_algorithm_iterator: ";
boost::progress_timer timer;
int i=0;
while( i++ < ciCount )
{
boost::algorithm::split_iterator< string::const_iterator > iter(
str, boost::algorithm::token_finder( bind2nd( equal_to<char>(), '*' ) )
);
boost::algorithm::split_iterator< string::const_iterator > end;
int index =0;
while( iter != end )
{
// 防止多余的Copy动作
// vec.push_back( string( boost::begin(*iter), boost::end(*iter) ) );
vec[index++].assign( boost::begin(*iter), boost::end(*iter) );
++iter;
}
}
}
{
cout<<"boost.tokenizer: ";
boost::progress_timer timer;
int i=0;
while( i++ < ciCount )
{
vec.clear();
boost::tokenizer< boost::char_separator<char> > tokens( str, boost::char_separator<char>("*") );
copy( tokens.begin(), tokens.end(), back_insert_iterator< STR_VEC >( vec ) );
}
}
#ifdef __HAVE_STL_SOFT__
vec.clear();
{
cout<< "stlsoft.string_tokeniser: ";
boost::progress_timer timer;
int i=0;
while( i++ < ciCount )
{
vec.clear();
stlsoft::string_tokeniser< string, char > tokens( str, '*' );
copy( tokens.begin(), tokens.end(), back_insert_iterator< STR_VEC >( vec ) );
}
}
#endif // #ifdef __HAVE_STL_SOFT__
return 0;
}; 我们对同样的字符串执行同样的拆分操作 2万次。在 Debug模式下面有输出: C-strtok_s: 0.42 s boost.string_algorithm_container: 7.56 s boost.string_algorithm_iterator: 5.55 s boost.tokenizer: 2.53 s stlsoft.string_tokeniser: 2.19 s 请按任意键继续 . . . 在 Release下面有输出: C-strtok_s: 0.04 s boost.string_algorithm_container: 0.14 s boost.string_algorithm_iterator: 0.05 s boost.tokenizer: 0.14 s stlsoft.string_tokeniser: 0.05 s 请按任意键继续 . . . 通过分析我们知道, C的自然是最快的,因为它不需要多余的 Copy动作,而其它的都会有这样的操作,同时 C库提供的字符串函数大多用汇编写的,所以比较快。但是当我们让编译器全速优化之后发现 boost字符串算法库的迭代器和 stlsoft的迭代器也是相当快的。最慢的当然是使用容器来存放结果了,因为它始终都会不断的拷贝。 在实际应用当中我们应该具体问题具体分析,根据操作的环境选择最适合的处理方式。举个简单的例子,比如我们只对拆分结果的第某个感兴趣,那么很显然 stlsoft的迭代器就更适合一些,为什么呢?刚才我们看过一些 boost.tokenizer的源码,知道在迭代器中它保存了一个 tok_作为结果的容器,每次自增都会将结果拷贝到这个 tok_中,也就是说每次自增都会有拷贝发生,而对于 stlsoft便不是如此,它在迭代器中保存的是两个迭代器所组成的范围,只要当我们提领的时候才会构造字符串:V operator *() const
{
return traits_type::create(m_find0, m_find1);
} 很明显这种效率会高于 boost.tokenizer。但是另外一个情况下,比如说我们需要多次提领同一迭代器,那么对于 stlsoft来说便不合适了,因为每次提领它都会构造,使得最终我们多次构造字符串。而 boost.tokenizer这时候更适合,因为它传回的是保存在内部的 tok_的常引用 :const Type& dereference() const
{
BOOST_ASSERT(valid_);
return tok_;
} 这也从另外一个角度证明了一个观点:没有绝对的好与不好,只有适合与不适合的问题。 作者: 董波