现代 C++ 零拷贝视图体系详解 —— std::span 与 std::string_view 完整指南
C++20 std::span 与 C++17 std::string_view 详解
引言
在 C++ 中,我们经常需要传递一段连续内存的“视图”(view),而不想拷贝数据或传递裸指针导致安全问题。C++17 引入了 std::string_view 专门用于字符串视图,C++20 则引入了通用的 std::span 用于任意类型的连续序列。理解两者的异同和应用场景,有助于写出更安全、高效的代码。
本文详细对比这两个视图类,并说明它们的使用方法和注意事项。
概览
| 特性 | std::string_view |
std::span<T> |
|---|---|---|
| 引入版本 | C++17 | C++20 |
| 头文件 | <string_view> |
<span> |
| 所表示的序列 | 字符序列(char、wchar_t、char8_t等) |
任意类型的连续内存序列(T 可为任意类型) |
| 是否拥有数据 | 否(非拥有视图) | 否(非拥有视图) |
| 可修改性 | 只读(const char 视图) |
可读写(取决于 T 是否为 const)例如 span<int> 可修改元素,span<const int> 只读 |
| 大小类型 | 动态大小(始终为动态) | 支持动态大小(span<T>)和静态大小(span<T, N>) |
| 空终止保证 | 不保证空终止(视图可能不包含 '\0') |
不适用 |
| 常见用途 | 字符串处理函数参数(避免拷贝 std::string) |
泛型函数处理连续序列(数组、vector、array 等) |
| 与 STL 算法 | 可传递 .begin()/.end() 给算法;C++20 范围版本也支持 |
可传递 .begin()/.end() 给算法;C++20 范围算法直接接受 span |
| 内存布局 | 通常包含一个指针和长度 | 通常包含一个指针和长度(动态大小),静态大小可能仅含指针 |
1. std::string_view
1.1 设计目的
std::string_view 旨在解决字符串传递时的拷贝开销问题。传统上,函数接受 const std::string& 虽然避免了拷贝,但需要调用者构造 std::string(可能从字符串字面量隐式转换,仍有小开销)。使用 string_view,可以接受任何形式的字符序列(std::string、const char*、std::string_view 自身),而不会拷贝数据。
1.2 基本用法
1 | |
1.3 常用操作
- 构造:可从
const char*、std::string、std::string_view及指针+长度构造。 - 成员函数:
size()、length()、data()、empty()、substr(pos, count)(返回新string_view)、remove_prefix(n)/remove_suffix(n)(修改视图范围,非 const 成员)、比较操作符、查找函数(find等,类似std::string)。 - 注意:
data()不保证空终止,切勿当作 C 字符串使用(除非你确定底层包含'\0')。
1.4 生命周期警告
string_view 不拥有数据,必须确保原字符串在视图使用期间存活:
1 | |
1.5 与 STL 算法
可将 string_view 的迭代器传递给算法:
1 | |
C++20 范围算法也可直接接受 string_view:
1 | |
2. std::span
2.1 设计目的
std::span 是 C++20 引入的通用连续序列视图。它类似于 string_view,但适用于任何类型的元素,并且支持修改(除非元素类型为 const)。常用于编写泛型函数,接收各种连续容器(std::vector、std::array、C 数组)而无需模板化容器类型。
2.2 基本用法
1 | |
2.3 静态大小与动态大小
std::span 可以带有编译期大小:
1 | |
动态大小是默认的:std::span<int>。静态大小的 span 不存储长度,只存指针,更轻量,但灵活性降低。
2.4 常用操作
- 构造:从指针+长度、迭代器对、各种连续容器(
std::vector、std::array、C 数组)隐式转换。 - 成员函数:
size()、size_bytes()、data()、empty()、first(n)、last(n)、subspan(pos, count)(返回新 span)。 - 修改:
span<T>允许通过迭代器或下标修改元素(若T非 const)。
2.5 与 const 的正确使用
1 | |
2.6 生命周期警告
与 string_view 类似,span 不拥有数据,必须保证原始数据生命周期长于 span。
2.7 与 STL 算法
span 可直接用于范围算法(C++20):
1 | |
传统迭代器对算法也可用:
1 | |
3. 关键区别与联系
3.1 类型安全性
string_view专用于字符,提供了字符串相关的成员(如find、substr返回string_view)。span通用,没有字符串特有操作,但可通过迭代器与算法配合实现类似功能。
3.2 可变性
string_view始终只读(类似span<const char>)。span的可变性由模板参数T控制,可读可写。
3.3 空终止符
string_view不保证空终止,需小心使用需要空终止的 C 函数。span与空终止无关,它只是连续内存块。
3.4 大小类型
string_view只有动态大小。span支持编译期静态大小,可优化性能。
3.5 相互转换
由于 string_view 本质是字符序列,可以看作一种特殊的 span,但两者没有直接的隐式转换。你可以通过构造函数显式转换:
1 | |
但注意:如果 span 指向的字符序列中间包含 '\0',string_view 会正确将其视为数据的一部分,而 C 字符串函数可能被截断,这取决于使用方式。
4. 使用场景建议
何时使用 std::string_view
- 函数参数需要接受多种字符串类型(
const char*、std::string)且不修改内容。 - 需要对字符串进行查找、子串等操作但不想拷贝。
- 作为字符串处理函数的返回值(例如提取子串),但必须确保原字符串存活。
何时使用 std::span
- 编写泛型函数处理任意连续序列(整数、自定义对象等),避免为每种容器重载。
- 需要传递一个数组的一段连续子区间(例如
subspan)。 - 在性能敏感代码中,用静态大小的
span避免存储长度开销。 - 配合 ranges 和算法进行数据操作。
何时避免使用视图
- 需要长期持有数据(应使用容器)。
- 需要传递空终止字符串给 C API(若视图不含空终止,则不安全)。
- 函数内部会保存视图(可能导致悬挂引用)。
5. 与之前讨论的 STL 算法的结合
在上一篇文章中,我们介绍了大量 STL 算法。视图类可以极大地方便算法调用:
示例:使用 string_view 简化字符串处理
1 | |
示例:使用 span 传递子范围给算法
1 | |
示例:C++20 范围算法直接接受视图
1 | |
6. 注意事项总结
- 生命周期:永远确保视图的源数据在使用期间有效。
- 不要存储视图:类成员尽量不要存储视图(除非你明确知道外部数据生命周期)。
- 空终止:使用
string_view的data()调用 C 字符串函数时,如果视图不包含空字符,会导致越界。可先用std::string构造临时副本,或确保视图完整且含空终止(但视图通常不含)。 - 迭代器失效:如果原始容器重新分配内存(如
vector的push_back导致扩容),现有视图会失效。 - 隐式转换:
std::vector等容器可以隐式转换为span,但要注意 const 正确性。
结语
std::string_view 和 std::span 是现代 C++ 中非拥有视图的重要工具,它们提升了代码的灵活性和安全性,同时避免了不必要的拷贝。理解它们的区别和适用场景,能帮助我们写出更高效、更清晰的代码。结合 STL 算法,视图类可以成为数据处理流水线中的关键一环。