[译]How to split a string in C++

这个问题是说, 怎么得到组成一句话的各个单词, 或者得到CSV中的各个数据片段. 这在C++中是个很简单的问题, 却有很多种答案.

有3种方案, 每种有利有弊. 使用时请自己选择最佳方案. 这篇文章的目的是说明 迭代器的接口是如何优胜于简单的容器的, 并且阐明 design of the STL 是何等强大.

方案1使用的标准组件(虽然方案1.2 做了微调). 方案2相对好点但使用了boost. 而方案3 更好但使用了ranges. 所以到底应该用哪个, 取决于你需要什么和你能使用什么.

Solution 1: Iterating on a stream

Stepping into the world of streams

“流” 是一个 能生成 与源或希望连接的目标 的联系 的对象. 流可以从源中获取信息(std::istream), 或为目标提供信息(std::ostream), 或者两者皆可(std::iostream).

源和目标可以是标准输入(std::cin), 标准输出(std::cout), 一个文件, 或者一个字符串, 前提是方式得当. 对流的主要操作包括: - 对于输入流: 使用操作符>> 从里面读取信息 - 对于输出流: 使用操作符<<, 向它推入信息

一个指向字符串的输入流, std::istringstream, 有个有趣的特性: 它的操作符>> 在源字符串中制造出去向下一个空格的字符串.

istream_iterator

std::istream_iterator 是连接输入流的迭代器. 它代表了输入迭代器的普遍接口, 但它的操作符++ 更像是输入流.

istream_iterator 以它从流里读取的类型为模板. 我们现在使用istream_iterator<std::string>, 它从流里读取字符串, 分离时为我们提供一个字符串.

当到达流的终点时, 流向它的迭代器发送信号, 然后迭代器被标记为结束.

Solution 1.1

现在, 我们可以借迭代器的接口使用算法, 这真切地证明了STL 设计的灵活性. 为了使用STL, 我们需要一个begin 和一个end (请参考Inserting several elements into an STL container efficiently). begin 是一个 还没开始着手分割的字符串的istreamstream 的迭代器: std::istream_iterator<std::string>(iss) . 按照惯例, end 的默认值也是个istream_iterator : std::istream_iterator<string>().

代码如下:

std::string text = "Let me split this into words";
std::istringstream iss(text);
std::vector<std::string> results((std::istream_iterator<std::string>(iss)), std::istream_iterator<std::string>());

第一个参数的额外的括号是为了避免与一个函数调用的歧义–请参考Scott Meyers的著作Effective STL 条目6 “most vexing parse”

优: - 仅使用标准组件 - 除字符串外, 对所有流都适用 劣: - 只能以空格为分隔符进行分割, 而且这在解析CSV时会是个至关重要的问题 - 在性能方面有待优化(但如果这不是影响你整个程序的瓶颈, 这也不是个大问题) - 很多人认为仅为了分割一个字符串, 写了太多代码

Solution1.2: Pimp my operator>>

导致上面两条劣势的原因是同一个: istream_iterator 从流里读取字符串时调用的操作符>>. 这个操作符做了很多事: 在下一个空格处停止(这是我们的最初的需求, 但这个不能自定义), 格式化, 读取然后设置一些标志位, 构造对象, 等等. 而以上这些, 大部分我们是不需要的. 所以我们希望自己实现下面的函数:

std::istream& operator>>(std::istream& is, std::string& output)
{
    // ...does lots of things...
}

实际上, 我们无法改变这些, 因为这是在标注库里的. 我们可以用另一个类型重载它, 但是这个类型需要是string 的一种.

所以现在的需求就是, 用另一种类型伪装成string. 有两种方案: 继承std::string 和 用显式转换封装string. 这里我们选择继承.

假如我们希望以逗号为分割符分割一个字符串:

class WordDelimitedByCommas: pulic std::string
{};

我必须承认这是有争议的. 有人会说:”std::string 没有虚析构函数, 所以你不应该继承它!” 这可能, 大概, 也许是有一点点点点武断. 这里我要说的是, 继承本身不会产生问题. 诚然, 当一个指向WordDelimitedByCommas 的指针以std::string 的形式被delete 掉时, 会产生问题. 继续读, 你会发现, 我们不会这么做. 现在我们可以阻止写代码的人借WordDelimitedByCommas 突发冷箭破坏程序吗? 我们不能. 但是这个险值得我们冒吗? 请继续读, 然后你自己判断.

现在为了仅实现我们需要的功能, 我们可以重载操作符>> : 获取下一个逗号之前的所有字符. 这个可以借用getline 函数实现:

std::istream& operator>>(std::istream* is, std::WordDelimitedByCommas&)
{
    std::getline(is, output, ',');
    return is;
}

返回值is 保证了可以连续调用操作符>>

现在我们可以写初级代码了:

std::string text = "Let,me,split,this,into,words";
std::istringstream iss(text);
std::vector<std::string> results((std::istream_iterator<WordDelimitedByCommas>(iss)), std::istream_iterator<WordDelimitedByCommas>());

我们可以通过模板化WordDelimitedByCommas 泛华所有的分隔符:

template<char delemiter>
class WordDelimitedBy: pulic std::string
{};

现在以分号举例:

std::string text = "Let;me;split;this;into;words";
std::istringstream iss(text);
std::vector<std::string> results((std::istream_iterator<WordDelimitedBy<';'>>(iss)), std::istream_iterator<WordDelimitedBy<';'>>());

优: - 编译时允许任何分隔符 - 不仅是字符串, 对任何流都可以操作 - 比方案1更快(快20%到30%) 劣: - 虽然可以很方便的复用, 但仍不是标准 - 仅仅为了分割一个字符串, 这个方案仍然使用了大量代码

Solution2: Using boost::split

这个方案比方案1高级, 除非你需要对所有的流都进行操作.

#include <boost/algorithm/string.hpp>

std::string text = "Let me split this into words";
std::vector<std::string> result;
boost::split<results, text, [](char c){return ' ' == c;});

传给boost::split 的第三个参数是一个函数或函数对象, 确定一个字符是不是分隔符. 上面的例子是使用lambda 表达式, 传入一个char, 返回这个char 是否是空格.

boost::split 的实现很简单: 在到达字符串的结束位置之前, 重复地调用find_if .

优: - 非常直观的接口 - 允许任何分隔符, 甚至是多个 - 高效: 比方案1.1 快 60% 劣: - 暂不是标准: 需要用到boost

Solution 3(未来): Usingranges

虽然它们现在还没有像标准库甚至boost 里的组件一样被广泛使用, rangesfuture of the STL . 在未来几年, 会大量面世.

Eric Neiber 的 range-v3 库 提供了非常友好的接口. 为了生成一个字符串的分割view, 代码如下:

std::string text = "Let me split this into words";
auto splitText = text | view::split(' ');

它有很多有趣的特性, 诸如 使用一个子字符串作为分隔符. ranges 会被C++20 引入, 所以我们应该能在几年之内就可以使用这个功能了.

So, how do I split my string?

如果你能使用boost, 务必使用方案2. 或者你可以自己写算法, 像boost 那样基于find_if 分割字符串.

如果你不想这么做, 你可以使用标准, 即方案1.1, 如果你需要自定义分隔符, 或者发现1.1是个瓶颈, 那么你可以选择方案1.2 .

如果你可以使用ranges , 那么就应该选择方案3.

翻译原文: http://www.fluentcpp.com/2017/04/21/how-to-split-a-string-in-c/