1. 字符串与文本处理曾经一直是C++的弱项,C++98标准仅提供了标准字符串类std::string,缺乏很多文本处理的高级特性,使得不少C++程序员不得不转向其他语言。C++11标准引入了<regex>、<codecvt>,支持原始字符串,但依然很不够。<br />Boost填补了C++这方面的空白,为整个C++社区做出了重要的贡献。①<br />本章将讨论Boost中五个字符串与文本处理领域的组件。首先是两个与C标准库函数功能类似的lexical_castformat,它们关注于字符串的表示,可以将数值转化为字符串,对输出做精确的格式化。接下来的string_ref提供了一个“只读视图”,可以避免字符串的拷贝代价,是更好的conststd::string&。后两个库关注字符串的处理:string_algo库提供了大量常用的字符串处理函数,可以满足90%以上的应用需求,剩下的10%可以使用xpressive,它是一个灵活且功能强大的正则表达式分析器,同时也是一个语法分析器。

5.1 lexical_cast

顾名思义,lexical_cast库进行“字面值”的转换,类似C中的atoi()函数,可以进行字符串与整数/浮点数之间的字面转换。<br />lexical_cast位于名字空间boost,需要包含头文件<boost/lexical_cast.hpp>。

5.1.1 函数声明

我们都很熟悉C语言中的atoi()、atof()系列函数,它们可以把字符串转换成数值,但这种转换是不对称的,不存在如itoa()这样的反向转换①,要想把数值转换为字符串,只能使用不安全的printf()。<br />    lexcical_cast使用类似C++标准转型操作符的形式给出了通用、一致、可理解的语法,它常用的两种形式是:
// 标准形式,转换数字和字符串
template <typename Target, typename Source>  
    inline Target lexical_cast(const Source &arg); 
// 转换 C 字符串
template <typename Target> 
    inline Target lexical_cast(const char* chars, std::size_t count);
lexcical_cast的第一种形式有两个模板参数,Target需要我们手工指定,是转换后的目标类型,通常是数字类型或者std::string;而第二个参数Source则不必写出,因为它可以出函数参数推导出来,所以调用的形式就是:
lexical_cast<Target>(...);    // Target=int/double/string/...
lexcical_cast的第二种形式专门处理C字符串,除了const char*外还支持const unsigned char*、const wchar_t*等其他字符类型,它只接受一个模板参数Target,指明转换后的目标类型。函数参数chars和count则标记了要转换的字符串的范围。

5.1.2 用法

使用lexcical_cast可以很容易地在数值与字符串之间转换,只需要在模板参数里指定转换的目标类型即可,例如:
#include <boost/lexical_cast.hpp>
#include <iostream>

using namespace std;
using namespace boost;

int main(int argc, char* argv[])
{
    int x = lexical_cast<int>("100");
    long y = lexical_cast<long>("2000");
    float pai = lexical_cast<float>("3.14159e5");
    double e = lexical_cast<double>("2.71828");
    double r = lexical_cast<double>("1.414, x", 5);

    cout << x << " " << y  << " " << pai  << " " << e  << " " << r;

    string str = lexical_cast<string>(465);
    cout << str << endl;

    cout << lexical_cast<string>(0.618) << endl;
    cout << lexical_cast<string>(0x10) << endl;

    return 0;
}
/*  程序的运行结果如下
    100 2000 314159 2.71828 1.414465
    0.61799999999999999
    16
*/
使用lexcical_cast时要注意,待转换成数字的字符串中只能有数字和小数点,不能出现字母或其他非数字字符(表示指数的e/E除外),也就是说,lexcical_cast不能转换如"123L"、"0x100"这样C++语法许可的数字字面值字符串。而且lexcical_cast不支持高级的格式控制,不能把数字转换成指定格式的字符串,如果需要更高级的格式控制,则应该用std::stringstream或者boost::format。<br />    除了转换数值与字符串,lexcical_cast也可以转换bool类型,但功能很有限,不能使用true/false字面值,只能使用1或0,例如
cout << lexical_cast<bool>("1") << endl;

5.1.3 错误处理

当lexcical_cast无法执行转换操作时会抛出异常bad_lexical_cast,它是std::bad_cast的派生类。<br />为了使程序更加健壮,在使用lexcical_cast转换时我们应当使用try/catch块来保护代码,例如:
#include <boost/lexical_cast.hpp>
#include <iostream>

using namespace std;
using namespace boost;

int main(int argc, char* argv[])
{
    try {                                            // try块里的每条语句都会引发异常
        cout << lexical_cast<int>("0x100");
        cout << lexical_cast<double>("HelloWorld");
        cout << lexical_cast<long>("1000L");
        cout << lexical_cast<bool>("false") << endl;
    } catch (bad_lexical_cast& e) {                  // 捕获异常
        cout << "error:" << e.what() << endl;
    }
    return 0;
}
/*  代码运行结果是
    error:bad lexical cast: source type value could not be interpreted as target
*/
lexical_cast在名字空间boost::conversion提供try_lexical_convert()函数,可以避免抛出异常,它以bool返回值表示是否转换成功。函数的声明是:①
template <typename Target, typename Source> 
    inline bool try_lexical_convert(const Source& arg, Target& result); 

template <typename Target, typename CharacterT> 
    inline bool try_lexical_convert( 
        const CharacterT* chars, std::size_t count, Target& result);
使用try_lexical_convert()可以安全地转换字面值,例如:
int x1;
assert(!conversion::try_lexical_convert("0x100", x1)); // 不需要捕获异常
cout << x1 << endl;
我们也可以利用try_lexical_convert()来验证数字字符串的合法性,实现一个模板函数num_valid():
template <typename T>
bool num_valid(const char* str) {
    T tmp;
    return conversion::try_lexical_convert(str, tmp); // 尝试转换数字
}
它的使用方法如下:
assert( num_valid<double>("3.14")); 
assert(!num_valid<int>("3.14")); 
assert( num_valid<int>("65535"));
这个小函数在验证用户输入有效性时会很有用。

①实际上lexical_cast()在内部就是调用的try_lexical_convert()。

5.1.4 转换对象的要求

虽然lexcical_cast的用法非常像转型操作符,但它仅仅是在用法上模仿了转型操作符而已,实际上是一个模板函数。<br />    lexcicalcast内部使用了标准库的流操作,因此,对于它的转换对象有如下要求:
  • 转换起点对象是可流输出的,即定义了operator<<;
  • 转换终点对象是可流输入的,即定义了operator>>;
  • 转换终点对象必须是可默认构造和可拷贝构造的。

    C++语言中的内建类型(int、double等)和std::string都满足以上的三个条件,它们也是lexcical_cast最常用的工作搭档。
    对于标准容器和其他用户自定义类型,这些条件一般都不满足,所以不能使用lexcical_cast。

    5.1.5 应用于自定义类

    如果我们想要将lexcical_cast应用于自定义的类,把类转换为可理解的字符串描述(类似于Java语言中的Object.toString()用法),只需要满足lexcical_cast的要求即可。准确地说,需要实现流输出操作符operator<<。
    下面的demo_class类重载了流操作符,可以用于lexcical_cast: ```cpp

    include

    include

using namespace std; using namespace boost;

class demo_class { friend std::ostream& operator<<(std::ostream& os, const demo_class& x) { os << “demo_class’s Name”; return os; } };

int main(int argc, char* argv[]) { cout << lexical_cast(demo_class()) << endl; return 0; }

/ demo_class’s Name /

    这段代码具有通用性,值得把它提取为一个模板类。我们可以仿造boost.operator库,定义一个模板类outable,以简化流输出操作符<<的重载。注意,这里没有使用基类链技术,不能用于operator的基类串联,但可以很容易添加这个功能:
```cpp
template<typename T>
struct outable {
    friend std::ostream& operator<<(std::ostream& os, const T& x) {
        os << typeid(T).name();     // 使用typeid操作符输出类型名称
        return os;
    }
};
这样,任何继承outable的类,都会自动获得流输出操作符或者lexcical_cast的支持。使用outable<T>,之前的例子就可以简化成:
class demo_class: outable<demo_class>{};
outable<T>的实现只是简单地运用typeid操作符输出了类的名字,如果想扩展outable<T>的功能,则可以考虑要求模板类型T提供to_string()或者print()之类的函数来输出特定信息。<br />    依据同样的原理,读者也可以实现流输入操作符>>的重载。

5.1.6 对比标准

C++标准增强了字符串与数字的互操作性,提供stoX()和to_string()函数实现了std::string与数字之间的转换(C++11.21.5),部分函数的声明如下:
// 字符串转数字
int stoi(const string& str, size_t *idx = 0, int base = 10); 
long stol(const string& str, size_t *idx = 0, int base = 10); 
long long stoll(const string& str, size_t *idx = 0, int base = 10); 
float stof(const string& str, size_t *idx = 0); 
double stod(const string& str, size_t *idx = 0);

// 数字转字符串
string to_string(Type val);
标准转换函数的用法与lexcical_cast类似,优点是无需写模板参数,而且允许字符串里出现非数字字符--它们会忽略起始的白空格,直到遇到无法转换的字符为止:
assert(stoi(" 42 ") == 42);           // 转换 int,允许有空格
assert(stol("100L") == 100L);         // 转换 long,支持 L 等后缀
assert(stol("1000 9") == 1000L);      // 转换 long,后面的被忽略
assert(stod("3.14ispi") == 3.14);     // 转换 double,遇到无效字符停止

assert(to_string(776ul) == "776");    // 转换为 string
但如果字符串不是以白空格、数字开头,或者超出了数字类型的范围,那么这些函数会抛出std::invalid_argument或者std::out_of_range异常:
cout << stoul("x100");                // 无法转换,抛出异常 invalid_argument 
cout << stoi("9999999999");           // 无法转换,抛出异常 out_of_range
很可惜,C++标准没有采用类似lexcical_cast的模板函数的方式,所以我们可以模仿lexcical_cast来包装它们方便使用,例如:
template<typename T> 
T std_lexical_cast(const std::string& s);              // 自定义模板转换函数
template<> 
int std_lexical_cast<int>(const std::string& s) {      // 特化为 int 
    return stoi(s);                                    // 调用 stoi 
} 
template<> 
long std_lexical_cast<long>(const std::string& s) {    // 特化为 long 
    return stol(s);                                    // 调用 stol 
} 
assert(std_lexical_cast<int>(" 10 ") == 10);           // 验证转换函数
assert(std_lexical_cast<long>("100L") == 100L);

读者可以借鉴这段代码实现完全的模板函数封装。

5.2 format

C++标准库提供了强大的、富有弹性的输入/输出流处理,使用流可以对输出的格式做各种精确的控制,如宽度、精度、进制、填充字符、对齐等,新式流输出操作符“<<”可以串联起任意数量的参数,非常自由。<br />    但C++输入/输出流也不是完美无瑕的,精确输出的格式控制要写大量的操控函数,而且会改变流的状态,用完后还需要及时恢复,有时候会显得十分烦琐。因此,还是有很多程序员怀念C语言中经典的printf(),虽然它缺乏类型安全检查,还有其他的一些缺点,但它语法简单高效,并且被广泛地接受和使用,影响深远。<br />    boost.format库“扬弃”了printf,实现了类似于printf()的格式化对象,可以把参数格式化到一个字符串,而且是完全类型安全的。<br />    format组件位于名字空间boost,需要包含头文件<boost/format.hpp>。

5.2.1 简单的例子

我们先通过一个简单的例子来了解format,看看它与printf()有什么相似和不同:
#include <boost/lexical_cast.hpp>
#include <iostream>

using namespace std;
using namespace boost;

int main(int argc, char* argv[])
{
    cout << format("%s:%d+%d=%d\n") % "sum" % 1 % 2 % (1 + 2);
    format fmt("(%1% + %2%) * %2% = %3%\n");
    fmt % 2 % 5;
    fmt % ((2 + 5) * 5);
    cout << fmt.str();
    return 0;
}
/*
sum:1+2=3
(2 + 5) * 5 = 35
*/
C/C++程序员都应该对这段代码有似曾相识的感觉—format的设计在很大程度上参照了printf(),但用法上却有很大的不同。<br />    代码里的第一条语句演示了format的最简单用法,使用format(...)构造了一个format临时对象。构造函数的参数是格式化字符串,其语法是我们非常熟悉的标准printf()语法,使用“%x”来指定参数的格式。<br />    因为要被格式化的参数个数是不确定的,printf()使用了C语言里的可变参数(即参数声明中的省略号),但它是不安全的。format模仿了流操作符<<,重载了二元操作符operator%作为参数输入符,它同样可以串联任意数量的参数,因此,`format(...) % a % b %c`,可以近似理解成:`format(...) << a << b << c`<br />    操作符%把参数逐个地“喂”给format对象,完成对参数的格式化。①<br />    最后,format对象支持流输出,可以直接向输出流cout输出内部保存的已格式化好的字符串。<br />第一条format语句的等价printf()调用是`printf("%s:%d+%d=%d\n","sum", 1, 2, (1+2))`<br />后面的三行语句演示了format的另一种用法,预先创建一个format格式化对象,它可以被后面的代码多次用于格式化操作。format对象仍然用操作符%来接受被格式化的参数,可以分多次输入(不必一次给全),但参数的数量必须满足格式化字符串的要求。最后,使用format对象的str()成员函数获得已格式好的字符串向cout流输出。<br />    第二个format用了略不同于printf()的格式化语法:" ( %1% + %2% ) * %2% = %3% \n ",有点类似C#语言,%N%可以指示参数的位置,减少参数输入的工作—这是对printf()语法的一个改进。<br />    第二个format对象的等价printf()调用是`printf("(%d + %d) * %d = %d\n",2, 5, 5, (2+5)*5);`

① 实际上,format 内部实现中就有一个函数名为 feed。

5.2.2 输入操作符

刚才的例子基本解释清楚了format组件的基本用法。大部分读者都会很容易理解它的格式化字符串,而对于操作符“%”可能就比较难理解。但是,选择operator%是经过了许多讨论而最终确定的,有很多的理由支持它。<br />**为什么使用重载操作符**<br />    format库在形成过程中考虑过许多其他形式的实现,比如完全仿造printf(),在函数里接受所有待格式化参数,像这样:`format("%s, %d...", x0, x1, ....);`<br />    基于类型安全的考虑,format不能使用省略号来实现可变参数。如果使用函数的调用形式,那么就需要定义不同参数数量的模板函数,如:
template <class T1, class T2, .., class TN> 
    string format(string s, const T1& x1, .... , const T1& xN);
如果C++编译器不支持可变参数模板特性,无论定义多少个这样的重载形式,都无法满足格式化“无限”个参数的需求。所以,为了保证代码的兼容性就必须使用操作符的方式来接受参数,而且输入/输出流的operator<<已经应用了许多年,证明这种方式的确有效。 <br />    二元操作符operator%()的声明如下:`format& operator%(T& x);`<br />    它接受format对象和任意值作为参数,然后返回format对象的引用,因此可以再对返回的format对象实施%操作符,所以`format("...") %x %y`被编译器解释为`operator% (format("..."), x) %y =>operator% (operator%(format("..."), x), y)`<br />    依次类推,从而能够接受无限个待格式化的参数。<br />**为什么使用 operator%**<br />    接下来的问题是为什么不使用operator<<,它可能比%更为广大程序员所熟悉,或者是operator[]、()、……<br />    operator<<已经被定义为用于流输出,如果format也使用operator<<,那么在编写代码的时候很容易造成概念上的混乱,编译器无法区分哪些<<是用于格式化,哪些<<是用于流输出,需要使用括号来限定format表达式。而且format本身也支持流输出,还有与其他算术运算符的优先级问题,都加重了混乱程度。因此,使用<<不是一个好的选择。<br />    operator[]、()和,也是可以采用的,比如assign库(4.4节)就重载了括号和逗号操作符,同样串联了多个参数,可以是这样:
format(...)[x][y][z] 
format(...)(x)(y)(z) 
format(...),x,y,z
这些操作符在技术上是完全可行的,但代码风格不如%简洁好看,也不能清晰地表明格式化操作的意图。<br />    最后是一些语言习惯方面的原因。原printf()用户都很熟悉格式化字符串中的%,这能够给他们带来亲切感,认识到这是一个格式化操作,可以降低学习门槛。而且,有些语言的print语句就已经使用了%来分隔参数。

5.2.3 类摘要

format并不是一个真正的类,而是一个typedef,真正的实现是basic_format,它的声明如下:
template< class charT, class Traits=std::char_traits<charT> > 
class basic_format; 
typedef basic_format<char> format;
basic_format的类摘要如下:
template <class charT, class Traits = std::char_traits<charT>>
class basic_format {
public:
    explicit basic_format(const charT* str);            // 构造函数
    explicit basic_format(const string_t& s);
    basic_format& operator=(const basic_format& x);

    string_t str() const;                               // 获得格式化后的字符串
    size_type size() const;                             // 获得字符串长度

    void clear();                                       // 清空缓存 
    basic_format& parse(const string_t&);               // 重新格式化

    template <class T> basic_format& operator%(T& x);   // 重载 operator%
    friend std::basic_ostream& operator << (...)        // 流输出
};

typedef basic_format<char> format;                      // 类型定义
typedef basic_format<wchar_t> wformat;                  // 类型定义

string str(const format&);                              // 自由转换函数
basic_format构造函数可以接受C字符串或std::string作为格式化字符串,格式化字符串使用类printf的格式规则。构造函数都被声明为explicit,因此我们只能使用显式构造。<br />    成员函数str()返回format对象内部已经格式化好的字符串(不清空),如果没有得到所有格式化字符串要求的参数则会抛出异常。format库还同时提供一个同名的自由函数str(),它位于boost名字空间,返回format对象内部已格式化好的字符串。<br />    成员函数size()可以获得已格式化好的字符串的长度,相当于str().size();同样,如果没有得到所有格式化字符串要求的参数则会抛出异常。<br />    成员函数parse()清空format对象内部缓存,并改用一个新的格式化字符串。如果仅仅想清空缓存,则可以使用clear(),它把format对象恢复到初始化状态。这两个函数执行后再调用str()或size()则会抛出异常,因为此时没有输入格式化参数。<br />    format重载了operator%,可以接受待格式化的任意参数。%输入的参数个数必须恰好等于格式化字符串要求的数量,过多或过少在format对象输出时都会导致抛出异常。在调用str()输出字符串或clear()清空缓冲区之后,则可以再次使用%。<br />    format还重载流输出操作符,因此可以直接向输入/输出流输出已格式化好的字符串,相当于向流输出str()。

5.2.4 格式化语法

format基本继承了printf的格式化语法,以便老程序员能够尽快掌握,它仅对printf语法有少量的不兼容,一般情况下我们很难遇到。<br />    每个printf格式化选项以%开始,后面是格式规则,规定了输出的对齐、宽度、精度、字符类型,我们都非常熟悉,例如:
  • %05d :输出宽度为5的整数,不足位用0填充;
  • %-8.3f :输出左对齐,总宽度为8,小数位3位的浮点数;
  • %10s :输出10位的字符串,不足位用空格填充;
  • %05X :输出宽度为5的大写十六进制整数,不足位用0填充。

    示范这几个格式化选项的代码如下:

    format fmt1("  %05d\n  %-8.3f\n  %10s\n  %05X\n");
    cout << fmt1 % 62 % 2.236 % "123456789" % 8;
    /*
    00062
    2.236   
    123456789
    00008
    */
    

    在经典的printf格式化外,format还增加了新的格式:

  • %|spec|:与printf格式选项功能相同,但两边增加了竖线分隔,可以更好地区分格式化选项与普通字符;

  • %N%: 标记第N个参数,相当于占位符,不带任何其他的格式化选项。

    使用%|spec|的形式,刚才的例子可以写成:

    format fmt2("%|05d|\n%|-8.3f|\n%| 10s|\n%|05X|\n");
    cout << fmt2 % 62 % 2.236 % "123456789" % 8;
    /*
    00062
    2.236   
    123456789
    00008
    */
    

    它的输出与printf格式的format完全相同,但看起来更清楚,尤其是对于%|10s|,可以很明显地看出有格式化参数“空格”。

    5.2.5 性能优化

    printf()不进行类型检查,直接向stdout输出,因此它的速度非常快,而format较printf()做了很多安全检查的工作,因此性能略差,速度上要慢一些。
    如果很在意format的性能,那么可以先建立constformat对象,然后拷贝这个对象进行格式化操作,这样比直接使用format对象能够提高速度,像这样:

    const format fmt("%10d %020.8f %010X %10.5e\n");     // 常量对象
    cout << format(fmt) %62 % 2.236 % 255 % 0.618;       // 拷贝使用
    

    5.2.6 高级用法

    format提供了类似printf的功能,但它并不等同于printf函数。在通常的格式化字符串之外,format类还拥有几个高级功能,可以在运行时修改格式化选项、绑定输入参数。
    这些高级功能用到的函数有:

  • basic_format&bind_arg(intargN,constT&val)
    把格式化字符串第argN位置的输入参数固定为val,即使调用clear()也保持不变,除非调用clear_bind()或clear_binds()。

  • basic_format& clear_bind(int argN)
    取消格式化字符串第 argN 位置的参数绑定。
  • basic_format& clear_binds()
    取消格式化字符串所有位置的参数绑定,并调用 clear()。
  • basic_format& modify_item(int itemN, T manipulator)
    设置格式化字符串第 itemN 位置的格式化选项,manipulator 是一个 boost::io::group()返回的对象。
  • boost::io::group(T1 a1, …, Var const& var)

    它是一个模板函数,最多支持10个参数(10个重载形式),可以设置输入/输出流操纵器以指定格式或输入参数值,输入/输出流操纵器位于头文件
    我们使用例子来说明这些函数的用法,为求简化,参数都是int: ```cpp

    include

    include

    include

using namespace std; using boost::io::group; // 打开名字空间

int main(){
// 声明 format 对象,有三个输入参数,五个格式化选项 format fmt(“%1% %2% %3% %2% %1% \n”); cout << fmt % 1 % 2 % 3;

fmt.bind_arg(2, 10);                                  // 将第二个输入参数固定为数字 10
cout << fmt % 1 % 3;                                  // 输入其余两个参数

fmt.clear();                                          // 清空缓冲,但绑定的参数不变
// 在%操作符中使用 group(),指定输入/输出流操纵符第一个参数显示为八进制
cout << fmt % group(showbase, oct, 111) % 333;
fmt.clear_binds();                                    // 清除所有绑定参数

// 设置第一个格式化项,十六进制,宽度为 8,右对齐,不足位用*填充
fmt.modify_item(1, group(hex, right, showbase, setw(8), setfill('*')));
cout << fmt % 49 % 20 % 100;

return 0;

} / 1 2 3 2 1 1 10 3 10 1 0157 10 333 10 0157 **0x31 20 100 20 49 /

<a name="N9P0G"></a>
### 5.3 string_ref
    在C++中处理字符串的基本工具是标准字符串std::string,但构造一个std::string成本较高,因为它必须完全持有字符串的内容,极端的时候会有很高的内存拷贝代价,影响程序效率。使用conststd::string&可以避免一些问题,但它在处理C字符串、提取子串时却又无能为力。总而言之,std::string显得有些“重”,我们需要一种更“轻”的字符串工具。<br />    boost.string_ref就是这样一种轻量级的字符串,顾名思义,它只持有字符串的引用,没有内存拷贝成本,所以运行效率很高,是更好的conststd::string&。它已经被收入C++17标准(但更名为string_view)。<br />    string_ref库位于名字空间boost,需要包含头文件<boost/utility/string_ref.hpp>。
<a name="gdopG"></a>
#### 5.3.1 类摘要
    string_ref库定义了basic_string_ref,接口与std::string很相似,下面的代码仅列出basic_string_ref的一些常用成员函数,省略了find和比较系列函数:
```cpp
template <typename charT, typename traits>
class basic_string_ref {
public:
    basic_string_ref();                                     //  构造函数
    basic_string_ref(const charT* str);
    basic_string_ref(const charT* str, size_type len);
    basic_string_ref(const std::basic_string& str);

    basic_string_ref(const basic_string_ref& rhs);          // 拷贝构造
    basic_string_ref& operator=(const basic_string_ref& rhs);

    size_type size() const;                                 // 字符串长度
    size_type length() const;
    size_type max_size() const;
    bool empty() const;

    const_iterator begin() const;                          // 迭代器
    const_iterator end() const;
    const_reverse_iterator rbegin() const;
    const_reverse_iterator rend() const;

    const charT& operator[](size_type pos) const;          // 获取字符
    const charT& at(size_t pos) const;
    const charT& front() const;
    const charT& back() const;
    const charT* data() const;                            // 返回字符指针

    void clear();                                         // 清空引用
    void remove_prefix(size_type n);                      // 删除前缀
    void remove_suffix(size_type n);                      // 删除后缀

    basic_string_ref substr(size_type pos, size_type n = npos) const;
    bool starts_with(charT c) const;
    bool starts_with(basic_string_ref x) const;
    bool ends_with(charT c) const;
    bool ends_with(basic_string_ref x) const;

    std::basic_string to_string() const;                  // 转换字符串
    explicit operator std::basic_string() const;

private:
    const charT* ptr_;                                   // 字符串指针
    std::size_t len_;                                    // 字符串长度
}
basic_string_ref的工作原理很简单,它不拷贝字符串,所以也就不分配内存,只用两个成员变量ptr_和len_标记字符串的起始位置和长度,这样就实现了字符串的表示。<br />    基于ptr_和len_,basic_string_ref实现了与std::string基本一致的接口,例如获取大小、获取字符、取字串、迭代器和比较等等。但需要注意,basic_string_ref是一个字符串的“常量视图”,大部分成员函数都是const修饰的,我们只能像conststd::string&那样去观察字符串而无法修改字符串,理解这一点非常重要。<br />    与std::basic_string类似,basic_string_ref也定义了几个typedef方便使用:
typedef basic_string_ref<char> string_ref;            // 标准字符串引用
typedef basic_string_ref<wchar_t> wstring_ref;        // 宽字符串引用 
typedef basic_string_ref<char16_t> u16string_ref;     // C++的新字符类型
typedef basic_string_ref<char32_t> u32string_ref;     // C++的新字符类型
接下来我们主要使用与std::string对应的string_ref。

5.3.2 用法

string_ref提供了数个构造函数,可以从字符数组、std::string或者它们的一部分构造出一个几乎没有成本的字符串“视图”:
#include <boost/utility/string_ref.hpp>
#include <iostream>

using namespace std;
using namespace boost;


int main(){
    const char* ch = "Days of Future Past";       // 字符数组
    string str(ch);                               // 标准字符串,有拷贝成本

    string_ref s1(ch);                            // 字符数组构造,零拷贝
    string_ref s2(str);                           // 标准字符串构造,零拷贝
    assert(s1 == s2 && s1 == ch && s2 == str);    // 比较操作

    string_ref s3(ch, 4);                         // 截取部分字符串构造
    assert(s3 == str.substr(0, 4));

    string_ref s4, s5;                            // 空构造
    s4 = ch;                                      // 赋值字符数组
    s5 = str;                                     // 赋值标准字符串
    assert(s4 == s5);

    return 0;
}
我们可以把string_ref当做标准字符串一样使用(除了修改操作):
#include <boost/utility/string_ref.hpp>
#include <iostream>

using namespace std;
using namespace boost;


int main(){
    const char* ch = "Apple iPhone iPad";     // 字符数组
    string_ref str(ch);                       // 创建字符串引用

    assert(!str.empty());                     // 是否空字符串
    assert(str.size() == strlen(ch));         // 获取长度

    for (auto& x : str) {                     // 支持新式for循环
        cout << x;
    }
    cout << endl;

    assert(str.front() == 'A');               // 类似容器的操作
    assert(str[1] == 'p');

    assert(str.find('i') == 6);               // 简单的查找操作

    auto substr = str.substr(6, 6);           // 获取子串,仍然是引用
    assert(substr == "iPhone");               // 与字符数组比较

    string s = str.to_string();               // 转换为标准字符串
    assert(str == s);                         // 与标准字符串比较

    str.clear();                              // 清空字符串
    assert(str.empty());

    return 0;
}
虽然string_ref不能直接改变原字符串,但它可以使用remove_prefix()和remove_suffix()这两个函数调整string_ref内部的字符串指针和长度,达到变动字符串引用的目的—但原始字符串仍然没有被修改:
string_ref str("Apple iPhone iPad");          // 直接从字符数组构造

str.remove_prefix(6);                         // “删除”前 6 个字符
assert(str.starts_with("iP")); 

str.remove_suffix(5);                         // “删除”后 5 个字符
assert(str.ends_with("one"));
由于string_ref的接口与string完全相同,所以它的一个重要用途是代替conststd::string&类型作为函数参数或者返回值,可以完全避免字符串拷贝代价,提高字符串的处理效率。<br />    下面的代码示范了string_ref作为函数参数和返回值的用法:
auto trunk = []                               // lambda 表达式 
(string_ref str)->string_ref{                 // 参数和返回类型都是 string_ref 
    return str.substr(0, 5); 
}; 
cout << trunk("abcdefg") << endl;             // 调用 lambda
函数对象trunk使用string_ref作为函数参数,它执行的操作只有少量的指针调整,所以执行效率很高。如果使用const std::string&,那么入口参数的字符数组会有一次拷贝,在返回时又会有一次拷贝,效率上就会差很多。<br />使用string_ref还需要加一点小心,因为它持有的是弱引用,必须保证被引用的字符串对象可用,尽量避免长期持有或者延后使用。在确实需要持有或者修改字符串的时候可以调用成员函数to_string()获得一个拷贝来保证安全。

5.4 string_algo

字符串标准类std::string有一些成员函数可以查找子串、访问字符,执行基本的字符串处理功能。由于std::string符合容器的定义,也可以把它看做是元素类型为char的序列容器,使用标准算法来对它进行运算。但标准算法并不是为字符串处理定制的,很多时候会显得有些“笨拙”。<br />    string_algo库的出现改变了这个局面。它是一个非常全面的字符串算法库,提供了大量的字符串操作函数,如大小写无关比较、修剪、特定模式的子串查找等,可以在不使用正则表达式的情况下处理大多数字符串相关问题。<br />    string_algo库位于名字空间boost::algorithm,但被using语句引入到了名字空间boost,需要包含头文件<boost/algorithm/string.hpp>。

5.4.1 简单示例

我们通过一个简单的例子来了解string_algo库中一些算法:
#include <boost/algorithm/string.hpp>
#include <iostream>

using namespace boost;
using namespace std;

int main()
{
    string str("readme.txt");                                      // 一个标准字符串
    if (ends_with(str, "txt")) {                                   // 判断后缀
        cout << to_upper_copy(str) + " UPPER" << endl;             // 大写
        assert(ends_with(str, "txt"));
    }

    replace_first(str, "readme", "followme");                      // 替换
    cout << str << endl;

    vector<char> v(str.begin(), str.end());                        // 一个字符的vector
    vector<char> v2 = to_upper_copy(erase_first_copy(v, "txt"));

    for (auto ch : v2) {                                           // 新式for循环
        cout << ch;
    }
    cout << endl;
    return 0;
}
/*
    README.TXT UPPER
    followme.txt
    FOLLOWME.
*/
这段代码示范了string_algo库中ends_with()、to_upper_copy()、replace_first()和erase_first_copy()等函数的基本用法,它们名称的含义都是自说明的,不言自明。<br />    值得注意的是代码后面对std::vector的操作,它说明了string_algo库的一个重要特征:库里包含的是泛型算法,因此可作用于容器上(但需符合一些要求),不必是string或者basic_string<T>。

5.4.2 算法概述

string_algo被设计用于处理字符串,然而它的处理对象并不一定是string或basic_string<T>,可以是任何符合boost.range①要求的容器。容器内的元素也不一定是char或wchar_t,任何可拷贝构造和赋值的类型均可,但如果类型的拷贝赋值代价很高,那么string_algo的性能会下降。<br />    当然,我们使用string_algo库主要的操作对象还是字符串string和wstring,它也可以工作在标准容器vector、deque、list和5.3节介绍的string_ref上。为求简便,接下来的讨论我们都基于std::string。<br />    string_algo库基于boost.range,因此避免了标准库算法必须提供begin()和end()迭代器的麻烦,也使得算法可以嵌套在一起“串联”处理字符串。<br />    string_algo库中的算法命名遵循了标准库的惯例,算法名均为小写形式,并使用不同的词缀来区分不同的版本,命名规则是:
  • 前缀i: 大小写不敏感(忽略大小写),否则是大小写敏感的;
  • 后缀_copy: 不变动输入,返回处理结果的拷贝,否则原地处理,输入即输出;
  • 后缀_if: 需要一个作为判断式的谓词函数对象,否则使用默认的判断准则。

    string_algo库提供的算法共分五大类:

  • 大小写转换;

  • 判断式与分类;
  • 修剪;
  • 查找与替换;
  • 分割与合并。

    接下来将对这些算法进行详细介绍(但不包含regex库相关的算法)。

    5.4.3 大小写转换

    string_algo库可以高效地实现字符串的大小写转换,包括两组算法:to_upper()和to_lower()。
    这两个算法的声明是(由于附加了前缀i和后缀_copy的算法的声明形式与原地处理的算法区别不大,为节省篇幅不再列出,以下同):

    template<typename T> void to_upper(T & Input); 
    template<typename T> void to_lower(T & Input);
    

    这两个算法具有后缀为_copy的版本。基本算法直接把输入在原地改变大小写,而copy版则不变动输入,返回修改后的拷贝,用法如下: ```cpp

    include

    include

using namespace boost; using namespace std;

int main() { string str(“FIreEmblem Heros\n”); cout << to_upper_copy(str); // 返回大写后的拷贝 cout << str; // 原字符串不改变 to_lower(str); // 字符串小写 cout << str; // 原字符串被改变 return 0; } / FIREEMBLEM HEROS FIreEmblem Heros fireemblem heros /

<a name="lxYWM"></a>
#### 5.4.4 判断式(算法)
    判断式算法可以检测两个字符串之间的关系,包括以下几种:

   - lexicographical_compare :根据字典顺序检测字符串是否小于另一个;
   - starts_with :检测字符串是否以另一个为前缀;
   - ends_with : 检测字符串是否以另一个为后缀;
   - contains :    检测字符串是否包含另一个;
   - equals :       检测两个字符串是否相等;
   - all :              检测字符串是否满足指定的判断式。①

    除了all,这些算法都有另一个i前缀的版本。由于它们不变动字符串,因此没有_copy版本。<br />    使用这些判断算法,可以很容易地解决困扰C++程序员多年的大小写无关字符串比较的问题:只需要使用带i前缀的算法,如ilexicographical_compare。<br />    这些算法的声明是:
```cpp
template<typename Range1T, typename Range2T> 
bool lexicographical_compare(const Range1T & Arg1, const Range2T & Arg2); 

template<typename Range1T, typename Range2T> 
bool starts_with(const Range1T & Input, const Range2T & Test); 

template<typename Range1T, typename Range2T> 
bool ends_with(const Range1T & Input, const Range2T & Test); 

template<typename Range1T, typename Range2T> 
bool contains(const Range1T & Input, const Range2T & Test); 

template<typename Range1T, typename Range2T> 
bool equals(const Range1T & Input, const Range2T & Test); 

template<typename RangeT, typename PredicateT> 
bool all(const RangeT & Input, PredicateT Pred);
另外要注意的是,它们的名称略微违背了string_algo库的命名原则,它们同时还有一种接受比较谓词函数对象的三参数版本,而没有使用_if后缀。谓词用于逐个地对字符串元素进行比较,相当于Pred(a[i],b[i])。<br />    示范这些算法的用法的代码如下:
#include <boost/algorithm/string.hpp>
#include <iostream>

using namespace boost;
using namespace std;

int main()
{
    string str("Power Bomb");
    assert(iends_with(str, "bomb"));                     // 大小写无关检测后缀
    assert(!ends_with(str, "bomb"));                     // 大小写敏感检测后缀

    assert(starts_with(str, "Pow"));                     // 检测前缀

    assert(contains(str, "er"));                         // 测试包含关系

    string str2 = to_lower_copy(str);                    // 转换小写
    assert(iequals(str, str2));                          // 大小写无关判断相等

    string str3("power suit");
    assert(ilexicographical_compare(str, str3));         // 大小写无关比较

    assert(all(str2.substr(0, 5), is_lower()));          // 检测字串均小写
    return 0;
}
这段代码大部分都很容易理解,最后一句话需要进行解释。str2.substr(0,5)返回str2前五个字符组成的子串,也就是“power”,is_lower()是接下来就要讲到的一个分类算法,它可以检测字符是否是小写字母。算法all把这两个参数组合起来,检测“power”是否全为小写字母组成。

5.4.5 判断式(函数对象)

string_algo增强了标准库中的equal_to和less函数对象,允许对不同类型的字符串进行比较,并提供大小写无关的形式。这些函数对象是:
  • is_equal : 类似 equals 算法,比较两个对象是否相等;
  • is_less : 比较两个对象是否具有小于关系;
  • is_not_greater : 比较两个对象是否具有不大于关系。

    示范这些函数对象用法的代码如下: ```cpp

    include

    include

    include

using namespace boost; using namespace std;

int main(int argc, char* argv[]) { string str1(“Samus”), str2(“samus”); assert(!is_equal()(str1, str2)); assert( is_less()(str1, str2)); assert(!is_equal()(str1, string_ref(str2))); // 使用 string_ref }

    注意函数对象名称后的两个括号,第一个括号调用了函数对象的构造函数,产生一个临时对象,第二个括号才是真正的函数调用操作符operator()。
<a name="1e7550bc"></a>
#### 5.4.6 分类
    string_algo提供一组分类函数,可以用于检测一个字符是否符合某种特性,主要用于搭配其他算法,比如5.4.4节的all算法。<br />    string_algo库的分类函数有下列数种:

   - is_space :      字符是否为空格或制表符(tab);
   - is_alnum :      字符是否为字母和数字字符;
   - is_alpha :       字符是否为字母;
   - is_cntrl :         字符是否为控制字符;
   - is_digit :         字符是否为十进制数字;
   - is_graph :       字符是否为图形字符;
   - is_lower :       字符是否为小写字符;
   - is_print :         字符是否为可打印字符;
   - is_punct :       字符是否为标点符号字符;
   - is_upper :       字符是否为大写字符;
   - is_xdigit :         字符是否为十六进制数字;
   - is_any_of :       字符是否是参数字符序列中的任意字符;
   - if_from_range : 字符是否位于指定区间内,即 from <= ch <= to。

    需要注意的是这些函数并不真正地检测字符,而是返回一个类型为detail::is_classifiedF的函数对象,这个函数对象的operator()才是真正的分类函数(因此,这些函数都属于工厂函数)。<br />    函数对象is_classifiedF重载了逻辑运算符||、&&和!,可以使用逻辑运算符把它们组合成逻辑表达式,以实现更复杂的条件判断。<br />    如果string_algo提供的这些分类判断式不能满足要求,那么我们也可以自己实现专用的判断式。方法很简单,定义一个返回值为bool的函数对象或者lambda表达式就可以了。例下面的代码判断字符是否为0或1,它等价于is_any_of("01"):
```cpp
struct is_zero_or_one {                    // 函数对象
     bool operator()(char x)  { 
        return x == '0' || x == '1';
    } 
}; 
auto is01 = [](char x) {                  // 更简单的 lambda 表达式
    return x == '0' || x == '1';
};
分类函数将结合其他算法示范其用法。

5.4.7 修剪

string_algo提供三个修剪算法:trim_left、trim_right和trim。<br />    修剪算法可以删除字符串开头或结尾部分的空格,它有_if和_copy两种后缀,因此每个算法都有四个版本。_if版本接受一个判断式IsSpace,将所有被判定为空格(IsSpace(c)==true)的字符删除。<br />    这些算法的声明是:
template<typename SequenceT> void trim_left(SequenceT & Input); 

template<typename SequenceT> void trim_right(SequenceT & Input); 

template<typename SequenceT> void trim(SequenceT & Input);
示范修剪算法用法的代码如下,使用format库(参见5.2节)进行输出格式化:
#include <boost/algorithm/string.hpp>
#include <iostream>
#include <boost/format.hpp>


using namespace boost;
using namespace std;

int main(int argc, char* argv[])
{
    format fmt(" |%s|\n");
    string str = "  samus aran    ";
    cout << fmt % trim_copy(str);                               // 删除两端的空格
    cout << fmt % trim_left_copy(str);                          // 删除左端空格

    trim_right(str);                                            // 原地删除右端的空格
    cout << fmt % str;

    string str2 = "2017 Happy new Year!!!";
    cout << fmt % trim_left_copy_if(str2, is_digit());          // 删除左端的数字
    cout << fmt % trim_right_copy_if(str2, is_punct());         // 删除右端的标点
    // 删除两端的标点、数字和空格
    cout << fmt % trim_copy_if(str2,is_punct() || is_digit() || is_space()); 
    return 0;
}
/*
 |samus aran|
 |samus aran    |
 |  samus aran|
 | Happy new Year!!!|
 |2017 Happy new Year|
 |Happy new Year|
*/
这段代码值得注意的是最后一行,它使用逻辑运算符组合了三个分类判断式,修剪了字串两端所有的标点、数字和空格。

5.4.8 查找

string_algo的查找算法提供与std::search()类似的功能,但接口不太一样。它不是返回一个迭代器(查找到的位置),而使用了boost.range库的iterator_range返回查找到的整个区间,获得了更多的信息,便于算法串联和其他处理(比如根据iterator_range的两个迭代器将原字符串拆成三份)。<br />    在这里简单介绍一下iterator_range。它概念上类似std::pair<iterator,iterator>,包装了两个迭代器,可以用begin()和end()访问,相当于定义了一个容器的子区间,并可以像原容器一样使用(更多的知识可参考推荐书目[3])。<br />    string_algo提供的查找算法包括下列几种:
  • find_first : 查找字符串在输入中第一次出现的位置;
  • find_last : 查找字符串在输入中最后一次出现的位置;
  • find_nth : 查找字符串在输入中的第 N 次(从 0 开始计数)出现的位置;
  • find_head : 取一个字符串开头 N 个字符的子串,相当于 substr(0,n);
  • find_tail : 取一个字符串末尾 N 个字符的子串。

    它们的声明是: ```cpp template iterator_range find_first(Range1T & Input, const Range2T & Search);

template iterator_range find_last(Range1T & Input, const Range2T & Search);

template iterator_range find_nth(Range1T & Input, const Range2T & Search, int Nth);

template iterator_range find_head(RangeT & Input, int N);

template iterator_range find_tail(RangeT & Input, int N);

    这些算法都不变动字符串,因此没有_copy后缀版本,但其中前三个算法有i前缀的版本。示范这些算法用法的代码如下:
```cpp
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
#include <boost/utility/string_ref.hpp>
#include <iostream>

using namespace boost;
using namespace std;

int main(int argc, char* argv[])
{
    format fmt("|%s|. pos = %d\n");

    string str = "Long long ago, there was a king.";

    iterator_range<string::iterator> rge;               // 迭代器区间

    rge = find_first(str, "long");                      // 找第一次出现
    cout << fmt % rge % (rge.begin() - str.begin());

    rge = ifind_first(str, "long");                     // 大小写无关找第一次出现
    cout << fmt % rge % (rge.begin() - str.begin());

    rge = find_nth(str, "ng", 2);                       // 找第三次出现
    cout << fmt % rge % (rge.begin() - str.begin());

    rge = find_head(str, 4);                            // 取前 4 个字符
    cout << fmt % rge % (rge.begin() - str.begin());

    rge = find_tail(str, 5);                            // 取末尾 5 个字符
    cout << fmt % rge % (rge.begin() - str.begin());

    rge = find_first(str, "samus");                     // 找不到
    assert(rge.empty() && !rge);

    return 0;
}
这段代码的最后两行说明iterator_range可以像标准容器一样判断是否为空,也可以隐式转换为bool值。<br />string_algo库还有另外三个查找算法:find_token、find_regex和通用的find算法,本书不做介绍,读者可阅读Boost文档了解其用法。

5.4.9 替换与删除

替换、删除操作与查找算法非常接近,是在查找到结果后再对字符串进行处理,所以它们的算法名称很相似。这些算法包括:
  • replace/erase_first : 替换/删除字符串在输入中的第一次出现;
  • replace/erase_last : 替换/删除字符串在输入中的最后一次出现;
  • replace/erase_nth : 替换/删除字符串在输入中的第 n 次出现;
  • replace/erase_all : 替换/删除字符串在输入中的所有出现;
  • replace/erase_head : 替换/删除输入的开头;
  • replace/erase_tail : 替换/删除输入的末尾。

    这些算法是一个相当大的家族。前八个算法每个都有前缀i、后缀_copy和组合,有四个版本,后四个则只有后缀_copy的两个版本。
    replace算法的声明是: ```cpp template void replace_first(SequenceT & Input, const Range1T & Search, const Range2T & Format);

template void replace_last(SequenceT & Input, const Range1T & Search, const Range2T & Format);

template void replace_all(SequenceT & Input, const Range1T & Search, const Range2T & Format);

template void replace_nth(SequenceT & Input, const Range1T & Search, int Nth, const Range2T & Format);

template void replace_head(SequenceT & Input, int N, const RangeT & Format);

template void replace_tail(SequenceT & Input, int N, const RangeT & Format);

    erase算法的声明是:
```cpp
template<typename SequenceT, typename RangeT> 
void erase_first(SequenceT & Input, const RangeT & Search); 

template<typename SequenceT, typename RangeT> 
void erase_last(SequenceT & Input, const RangeT & Search);

template<typename SequenceT, typename RangeT> 
void erase_nth(SequenceT & Input, const RangeT & Search, int Nth); 

template<typename SequenceT, typename RangeT> 
void erase_all(SequenceT & Input, const RangeT & Search); 

template<typename SequenceT> 
void erase_head(SequenceT & Input, int N); 

template<typename SequenceT> 
void erase_tail(SequenceT & Input, int N);
示范替换与删除算法用法的代码如下:
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
#include <boost/utility/string_ref.hpp>
#include <iostream>

using namespace boost;
using namespace std;

int main(int argc, char* argv[])
{
    string str = "Samus beat the monster.\n";

    cout << replace_first_copy(str, "Samus", "samus");

    replace_last(str, "beat", "kill");
    cout << str;

    replace_tail(str, 9, "ridley.\n");
    cout << str;

    cout << ierase_all_copy(str, "samus");
    cout << replace_nth_copy(str, "l", 1, "L");
    cout << erase_tail_copy(str, 8);
    return 0;
}
/*
Samus kill the samus beat the monster.
Samus kill the monster.
Samus kill the ridley.
 kill the ridley.
Samus kilL the ridley.
Samus kill the 
*/

5.4.10 分割

string_algo提供了两个字符串分割算法:find_all(虽然它的名称含有find,但因为其功能而被归类为分割算法)和split,可以使用某种策略把字符串分割成若干部分,并将分割后的字符串拷贝存入指定的容器。<br />    分割算法对容器类型的要求是必须能够持有查找到结果的拷贝或者引用,因此容器的元素类型必须是string或者iterator_range<string::iterator>,容器则可以是vector、list、deque等标准容器。<br />这两个算法的声明是:
template<typename SequenceSequenceT, typename Range1T, typename Range2T> 
    SequenceSequenceT & find_all(SequenceSequenceT & Result, Range1T & Input, 
                                 const Range2T & Search); 

template<typename SequenceSequenceT, typename RangeT, typename PredicateT> 
    SequenceSequenceT & split(SequenceSequenceT & Result, RangeT & Input, 
                              PredicateT Pred,  
                              token_compress_mode_type eCompress = token_compress_off);
find_all算法类似于普通的查找算法,它搜索所有匹配的字符串,加入到容器中,有一个忽略大小写的前缀i版本。<br />    split算法使用判断式Pred来确定分割的依据,如果字符ch满足判断式Pred(Pred(ch)==true),那么它就是一个分割符,将字符串从这里分割。<br />    参数eCompress可以取值为token_compress_on或token_compress_off,如果值为前者,那么当两个分隔符连续出现时将被视为一个,如果值为后者则两个连续的分隔符标记了一个空字符串。参数eCompress默认取值为token_compress_off。<br />    示范分割算法用的代码法如下:
string str = "Samus,Link.Zelda::Mario-Luigi+zelda";

deque<string> d;
ifind_all(d, str, "zELDA"); //大小写无关分割字符串
assert(d.size() == 2);
for (auto x : d) { //for 遍历 deque 里的字符串
    cout << "[" << x << "] ";
}
list<iterator_range<string::iterator>> l; //存储 range 对象
split(l, str, is_any_of(",.:-+")); //使用标点分割
for (auto x : l) { //for 遍历 list 里的字符串
    cout << "[" << x << "]";
}
l.clear();
split(l, str, is_any_of(".:-"), token_compress_on);
for (auto x : l) { //for 遍历 list 里的字符串
    cout << "[" << x << "]";
}
程序首先示范了find_all算法,使用deque<string>接收忽略大小写查找到的所有字符串。接着我们使用split算法,使用“,.:-+”中的任意字符分割字符串,由于原串中有两个“:”,因此分割的结果会有一个空串。最后的split算法使用了token_compress_on参数,并缩小了分隔符的取值范围。
[Zelda] [zelda] 
[Samus][Link][Zelda][][Mario][Luigi][zelda]
[Samus,Link][Zelda][Mario][Luigi+zelda]

5.4.11 合并

合并算法join是分割算法的逆运算,它把存储在容器中的字符串连接成一个新的字符串,并且可以指定连接的分隔符。<br />    合并算法的声明是:
template<typename SequenceSequenceT, typename Range1T> 
range_value< SequenceSequenceT >::type 
    join(const SequenceSequenceT & Input, const Range1T & Separator);
join还有一个后缀_if的版本,它接受一个判断式,只有满足判断式的字符串才能参与合并。<br />示范合并算法用法的代码如下:
#include <boost/assign/list_of.hpp>
#include <iostream>
using namespace boost::assign;

int main(int argc, char* argv[])
{
    vector<string> v = list_of("Samus")("Link")("Zelda")("Mario");
    cout << join(v, "+") << endl;                                    // 简单合并
    cout << join_if(v, "**",                                         // 带谓词的合并
    [](string_ref s)                                                 // lambda 表达式
    { 
        return contains(s, "a"); }                         // 包含字符 a
    );
    return 0;
}
/*
Samus+Link+Zelda+Mario
Samus**Zelda**Mario
*/
程序首先使用assign库向vector添加了四个字符串,然后以“+”合并它们。随后的join_if算法使用lambda表达式定义了一个简单的谓词,它包装了算法contains,判断字符串是否包含字符a。

5.4.12 查找(分割)迭代器

在通用的find_all或split之外,string_algo库还提供两个查找迭代器find_iterator和split_iterator,它们可以在字符串中像迭代器那样遍历匹配,执行查找或者分割,无需使用容器来容纳。<br />    使用查找迭代器可以实现某些简单算法无法实现的功能:
string str("Samus||samus||mario||||Link");
// 查找迭代器类型定义
typedef find_iterator<string::iterator> string_find_iterator;
string_find_iterator pos, end; // 声明查找迭代器变量
for (pos = make_find_iterator(str, first_finder("samus", is_iequal())); pos != end; ++pos) {
    cout << "[" << *pos << "]";
}
cout << endl;

// 分割迭代器类型定义
typedef split_iterator<string::iterator> string_split_iterator;
string_split_iterator p, endp; // 声明分割迭代器变量
for (p = make_split_iterator(str, first_finder("||", is_iequal()));
     p != endp; ++p) {
    cout << "[" << *p << "]";
}
cout << endl;
使用查找迭代器首先要声明迭代器对象find_iterator或split_iterator,它们的模板类型参数是一个迭代器类型,例如string::iterator或者char*。<br />    为了获得迭代器的起始位置,我们需要调用first_finder()函数,它用于判断匹配的对象,然后再用make_find_iterator或make_split_iterator来真正创建迭代器。同族的查找函数还有last_finder、nth_finder、token_finder等,它们的含义与查找算法类似,从不同的位置开始查找返回迭代器。<br />    初始化工作完成后,我们就可以像使用标准迭代器或者指针那样,不断地遍历迭代器对象,使用解引用操作符获取查找的内容,直到找不到匹配的对象。<br />    这里要特别注意一下分割迭代器的运用,它可以以任意长度的字符串作为分隔符进行分割,而普通的split算法则只能以字符作为分隔符。<br />    适当使用auto/decltype关键字可以简化查找迭代器的声明:
auto pos = //利用 auto 推导迭代器类型
    make_find_iterator(str, first_finder("samus", is_iequal()));
decltype(pos) end; //利用 decltype 推导类型
for (; pos != end; ++pos) {
    cout << "[" << *pos << "]";
}