这个问题是说, 怎么得到组成一句话的各个单词, 或者得到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
里的组件一样被广泛使用, ranges
是future 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/