2019-10-22 更新:感谢 @依云 在评论区提到了 Lazy Evaluation,这个特性在未来 C++ Coroutine 加入标准后将很容易、很优雅地实现。但据我所知,很多第三方库(不限于 C++ 语言)都提供了两套 API,支持实时计算和 Lazy Evaluation。我认为即使确实存在 Lazy Evaluation 可能在一定程度上提升性能的场景,实时计算也是存在充足的需求的,至少它省去了维护 Coroutine 相关数据结构的开销。
我猜应该很多人都手写过 split 吧,我也曾经在自己的项目中写过类似这样的函数。虽然我没有深入研究过 std::string
的标准,但根据我个人的经验,std::string
没有提供 split
成员函数可能有以下原因:
std::basic_string
,还有 std::basic_string_view
,甚至 C++20 之后还可能加入一些 Concepts(我不确定),所以仅把 split
作为 std::basic_string
的成员函数有失偏颇。不仅被分割的字符串可能有各种各样的类型,分割符、保存结果的容器在用户手里都可能具有不同的类型。即使不支持第一点中所述任一功能,仅支持这些不同类型的语义就会十分复杂。std::vector
;这样对于大部分用户来说虽然很难产生性能问题,但对于 STL 来说这种强依赖是不能接受的。
不过即使上述问题客观存在,我相信办法总比问题多。即使 split
在短时间内可能很难加入标准,我们也可以设计出符合我们需求的 split
函数。
如果大家感兴趣可以参考下我之前项目中使用的版本(我自己手写的),不支持第一点中的那些特性,但基本解决第 2、3 点问题,算是抛砖引玉,欢迎大家吐槽。
首先是函数签名:
template <class Str, class Spt, class C = std::vector</* see below */>> C split(const Str& str, const Spt& splitter, C result = C{});
模板类型说明:
Str
: 待分割字符串类型,可以是 std::basic_string
、std::string_view
、C-style 字符串或其他具有 find
、substr
的自定义类型。
Spt
: 分割符,可以是 Str
的任何类型或字符类型。
C
: 结果容器,默认为 std::vector
实例,支持定制。
实现(C++17):
#include <type_traits> #include <string> #include <string_view> #include <vector> namespace detail { template <class SFINAE, class Str> struct sfinae_is_general_string : std::false_type {}; template <class Str> struct sfinae_is_general_string<std::enable_if_t<std::is_convertible_v< decltype(std::declval<Str>().length()), std::size_t>>, Str> : std::true_type {}; template <class Str> decltype(auto) normalize_str(const Str& s) { if constexpr (std::is_pointer_v<Str>) { return std::basic_string<std::remove_pointer_t<Str>>{s}; } else if constexpr (std::is_array_v<Str>) { return std::basic_string<std::remove_extent_t<Str>>{s}; } else { return s; } } template <class Spt> decltype(auto) normalize_splitter(const Spt& s) { if constexpr (std::is_pointer_v<Spt>) { return std::basic_string_view<std::remove_pointer_t<Spt>>{s}; } else if constexpr (std::is_array_v<Spt>) { return std::basic_string_view<std::remove_extent_t<Spt>>{s}; } else if constexpr (sfinae_is_general_string<void, Spt>::value) { return s; } else { return std::basic_string<Spt>(1u, s); } } template <class Str> using normalized_str_t = std::decay_t<std::invoke_result_t< decltype(normalize_str<Str>), Str>>; template <class Str, class Spt, class Cp> void do_split(const Str& str, const Spt& splitter, Cp* result) { std::size_t substr_begin = 0u, substr_end; while ((substr_end = str.find(splitter, substr_begin)) != Str::npos) { result->push_back(str.substr(substr_begin, substr_end)); substr_begin = substr_end + splitter.length(); } result->push_back(str.substr(substr_begin, str.size())); } } // namespace detail template <class Str, class Spt, class C = std::vector<detail::normalized_str_t<Str>>> C split(const Str& str, const Spt& splitter, C result = C{}) { detail::do_split(detail::normalize_str(str), detail::normalize_splitter(splitter), &result); return result; }
来自Stack Overflow
For what it's worth, here's another way to extract tokens from an input string, relying only on standard library facilities. It's an example of the power and elegance behind the design of the STL.
#include <iostream> #include <string> #include <sstream> #include <algorithm> #include <iterator> int main() { using namespace std; string sentence = "And I feel fine..."; istringstream iss(sentence); copy(istream_iterator<string>(iss), istream_iterator<string>(), ostream_iterator<string>(cout, " ")); }
Instead of copying the extracted tokens to an output stream, one could insert them into a container, using the same generic copy algorithm.
vector<string> tokens; copy(istream_iterator<string>(iss), istream_iterator<string>(), back_inserter(tokens));
... or create the vector directly:
Split a string in C++?vector<string> tokens{istream_iterator<string>{iss}, istream_iterator<string>{}};
C++11以前有很多原因不能提供一个通用的split,比如说需要考虑split以后的结果存储在什么类型的容器中,可以是vector、list等等包括自定义容器,很难提供一个通用的;再比如说需要split的源字符串很大的时候运算的时间可能会很长,所以这个split最好是lazy的,每次只返回一条结果。
C++11之前只能自己写,我目前发现的史上最优雅的一个实现是这样的:
void split(const string& s, vector<string>& tokens, const string& delimiters = " ") { string::size_type lastPos = s.find_first_not_of(delimiters, 0); string::size_type pos = s.find_first_of(delimiters, lastPos); while (string::npos != pos || string::npos != lastPos) { tokens.push_back(s.substr(lastPos, pos - lastPos));//use emplace_back after C++11 lastPos = s.find_first_not_of(delimiters, pos); pos = s.find_first_of(delimiters, lastPos); } }
从C++11开始,标准库中提供了regex,regex用来做split就是小儿科了,比如:
std::string text = "Quick brown fox."; std::regex ws_re("\s+"); // whitespace std::vector<std::string> v(std::sregex_token_iterator(text.begin(), text.end(), ws_re, -1), std::sregex_token_iterator()); for(auto&& s: v) std::cout<<s<<"
";
C++17提供的string_view可以加速上面提到的第一个split实现,减少拷贝,性能有不小提升,参看此文:Speeding Up string_view String Split Implementation。
从C++20开始,标准库中提供了ranges,有专门的split view,只要写str | split(' ')就可以切分字符串,如果要将结果搜集到vector<string>中,可以这样用(随手写的,可能不是最简):
string str("hello world test split"); auto sv = str | ranges::views::split(' ') | ranges::views::transform([](auto&& i){ return i | ranges::to<string>(); }) | ranges::to<vector>(); for(auto&& s: sv) { cout<<s<<"
"; }
其实C语言里面也有一个函数strtok用于char*的split,例如:
#include <string.h> #include <iostream> #include <string> using namespace std; int main() { string str = "one two three four five"; char *token = strtok(str.data(), " ");// non-const data() needs c++17 while (token != NULL) { std::cout << token << '
'; token = strtok(NULL, " "); } } //如你所愿,输出如下: one two three four five
这里要注意的是strtok的第一个参数类型是char*而不是const char*,实际上strtok的确会改变输入的字符串。
参考文献:
欢迎关注个人公众号「树屋编程」,专注分享C++的学习知识与程序员的职场体验
本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度,google,bing,sogou 等
© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有