在C++17之后,标准库引入了std::string_view,它提供对连续字符序列的非拥有视图;到了C++20,Ranges库让我们可以以声明式管线处理序列。将二者结合,字符串分割可以做到不拷贝原数据、不依赖可变全局状态,并且代码表达意图清晰。下面先看一个基础示例,再逐步展开原理与陷阱。

为什么旧方式不够优雅
早期C++常用strtok做分割,它在内部保存静态指针,修改原字符串并插入结束符,这导致无法用于只读数据,且在多线程中必须加锁。另一种常见写法是使用std::stringstream配合getline,虽然安全,但每次提取都会构造临时std::string对象,对大文本或高频调用来说开销明显。
如果自己写find加substr的循环,逻辑虽然可控,但substr同样会复制子串。当只需要逐段读取内容、并不需要长期持有每段时,复制是完全多余的。我们希望有一种方式:只记录每段起止位置,按需取用,并且语法上像流水线一样直白。
string_view如何避免拷贝
std::string_view内部通常只保存两个成员:指向字符的指针和长度。构造它不会分配内存,也不会复制字符。我们可以用它对原字符串的某一段建立视图,从而把“分割”变成“标记边界”的动作。下面的函数展示了一个手写的零拷贝分割,返回一组string_view。
#include <string_view>
#include <vector>
std::vector<std::string_view> split_view(std::string_view str, char delim) {
std::vector<std::string_view> result;
size_t start = 0;
while (true) {
size_t pos = str.find(delim, start);
if (pos == std::string_view::npos) {
result.push_back(str.substr(start));
break;
}
result.push_back(str.substr(start, pos - start));
start = pos + 1;
}
return result;
}
这段代码中,substr调用的是string_view的版本,返回新的string_view而非std::string。调用方若需要持久化某一段,再把它转成std::string即可,做到“按需复制”。不过手写循环仍然偏命令式,接下来看Ranges如何更紧凑地表达同样逻辑。
用Ranges的views::split做声明式分割
C++20的Ranges提供了std::views::split,它接受一个范围和一个分隔符(或子范围),惰性产生每个“段”的子范围。配合string_view,我们可以这样写:
#include <string_view>
#include <vector>
#include <ranges>
void use_ranges_split() {
std::string_view text = "apple,banana,cherry";
auto segments = text
| std::views::split(',')
| std::views::transform([](auto rng) {
return std::string_view(rng.begin(), rng.end());
});
for (std::string_view sv : segments) {
// sv是零拷贝视图,可按需转std::string
}
}
注意views::split产生的每个元素是某个“子范围”类型,并不是直接的string_view,因此用transform包了一层构造。由于整个管线是惰性的,遍历时才真正切分,没有中间容器开销。相比手写循环,意图一眼可见:先按逗号拆,再把每段转成视图。
若编译器支持C++23的std::views::split对string_view更友好,还可直接用范围构造。但在C++20下,上面的写法已经足够清晰且安全。需要提醒的是,惰性视图绑定到原字符串生命周期,原串销毁后视图即悬空,使用时要保证作用域正确。
处理多字符分隔与连续分隔符
当分隔符是字符串而非单个字符时,views::split也接受初始化列表或子范围。例如按"::"分割,可传入std::string_view("::")。连续分隔符会产生空段,这与split_view手写版一致;如果希望跳过空段,可在管线后追加views::filter判断长度。
#include <string_view>
#include <ranges>
void skip_empty() {
std::string_view text = "a,,b";
auto seg = text
| std::views::split(',')
| std::views::transform([](auto r) {
return std::string_view(r.begin(), r.end());
})
| std::views::filter([](std::string_view v) {
return !v.empty();
});
}
这种组合展示了Ranges的扩展能力:每个适配器只关心自己的职责,拼接起来就是完整策略。对比传统方式,你不需要在循环里写一堆if判断空串,逻辑更扁平。当然,过滤器会增加一点遍历成本,但在大多数配置解析、文本导入场景中可以忽略。
性能与适用场景小结
从性能角度看,string_view加Ranges的方案避免了临时std::string分配,对几百KB到几MB的文本内容,耗时通常低于stringstream方案。它适合接口层接收大块只读文本、日志解析、协议报文拆包等场景。若分割后每段都需要独立修改并长期保存,那么在末端统一转成std::string即可,复制成本只发生在真正需要时。
在可读性上,Ranges管线让“分割—转换—过滤”变成链式描述,比手写状态机更容易维护。建议在新项目中优先采用该组合,并为旧代码提供兼容的split_view工具函数,逐步替换strtok与getline写法,降低隐藏的线程安全风险。
string_viewstd_rangesstring_split修改时间:2026-08-01 14:27:38