现代 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>
所表示的序列 字符序列(charwchar_tchar8_t等) 任意类型的连续内存序列(T 可为任意类型)
是否拥有数据 否(非拥有视图) 否(非拥有视图)
可修改性 只读const char 视图) 可读写(取决于 T 是否为 const)
例如 span<int> 可修改元素,span<const int> 只读
大小类型 动态大小(始终为动态) 支持动态大小(span<T>)和静态大小(span<T, N>
空终止保证 不保证空终止(视图可能不包含 '\0' 不适用
常见用途 字符串处理函数参数(避免拷贝 std::string 泛型函数处理连续序列(数组、vectorarray 等)
与 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::stringconst char*std::string_view 自身),而不会拷贝数据。

1.2 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <string_view>
#include <iostream>

void print_length(std::string_view sv) {
std::cout << "Length: " << sv.size() << '\n';
}

int main() {
// 从字符串字面量构造(不包含空终止符长度)
print_length("Hello"); // 输出 5

// 从 std::string 构造
std::string s = "World";
print_length(s); // 输出 5

// 从 const char* 指定长度构造
print_length(std::string_view("Hello, world", 5)); // 输出 5
}

1.3 常用操作

  • 构造:可从 const char*std::stringstd::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
2
3
4
std::string_view get_view() {
std::string s = "temporary";
return s; // 危险!s 在函数结束时销毁,返回的视图悬挂
}

1.5 与 STL 算法

可将 string_view 的迭代器传递给算法:

1
2
std::string_view sv = "C++17";
auto it = std::find(sv.begin(), sv.end(), '+');

C++20 范围算法也可直接接受 string_view

1
std::ranges::for_each(sv, [](char c){ std::cout << c; });

2. std::span

2.1 设计目的

std::span 是 C++20 引入的通用连续序列视图。它类似于 string_view,但适用于任何类型的元素,并且支持修改(除非元素类型为 const)。常用于编写泛型函数,接收各种连续容器(std::vectorstd::array、C 数组)而无需模板化容器类型。

2.2 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <span>
#include <vector>
#include <iostream>

// 接受任意 int 序列的视图
void process(std::span<int> sp) {
for (int& x : sp) {
x *= 2; // 可以修改
}
}

int main() {
std::vector<int> v = {1,2,3};
process(v); // vector 可隐式转换为 span<int>

int arr[] = {4,5,6};
process(arr); // C 数组可转换

std::array<int, 3> a = {7,8,9};
process(a); // std::array 可转换

// 也可以显式构造
std::span<int> sp(v.data(), 2); // 只取前两个元素
}

2.3 静态大小与动态大小

std::span 可以带有编译期大小:

1
2
3
void process_fixed(std::span<int, 3> sp) {  // 只接受大小为3的序列
// ...
}

动态大小是默认的:std::span<int>。静态大小的 span 不存储长度,只存指针,更轻量,但灵活性降低。

2.4 常用操作

  • 构造:从指针+长度、迭代器对、各种连续容器(std::vectorstd::array、C 数组)隐式转换。
  • 成员函数size()size_bytes()data()empty()first(n)last(n)subspan(pos, count)(返回新 span)。
  • 修改span<T> 允许通过迭代器或下标修改元素(若 T 非 const)。

2.5 与 const 的正确使用

1
2
3
4
5
6
7
8
9
10
void read_only(std::span<const int> sp) { /* 只能读 */ }
void read_write(std::span<int> sp) { /* 可读写 */ }

std::vector<int> vec = {1,2,3};
read_only(vec); // OK,vector<int> -> span<const int>
read_write(vec); // OK,vector<int> -> span<int>

const std::vector<int> cvec = {1,2,3};
read_only(cvec); // OK,const vector -> span<const int>
// read_write(cvec); // 错误,不能将 const 转为 span<int>

2.6 生命周期警告

string_view 类似,span 不拥有数据,必须保证原始数据生命周期长于 span。

2.7 与 STL 算法

span 可直接用于范围算法(C++20):

1
2
3
std::vector<int> v = {5,2,8,1};
std::span<int> s = v;
std::ranges::sort(s); // 排序 v 的前部分(实际是整个 v)

传统迭代器对算法也可用:

1
std::reverse(s.begin(), s.end());

3. 关键区别与联系

3.1 类型安全性

  • string_view 专用于字符,提供了字符串相关的成员(如 findsubstr 返回 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
2
3
4
5
std::string_view sv = "hello";
std::span<const char> sp(sv.data(), sv.size()); // string_view -> span<const char>
// 反向:span<const char> -> string_view 需要确保字符且无中间空字符
std::span<const char> sp2 = ...;
std::string_view sv2(sp2.data(), sp2.size());

但注意:如果 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
2
3
std::string_view text = "apple,banana,grape";
auto first_comma = std::find(text.begin(), text.end(), ',');
std::string_view fruit(text.begin(), first_comma); // "apple"

示例:使用 span 传递子范围给算法

1
2
3
std::vector<int> data(100);
std::span<int> first_ten(data.data(), 10);
std::sort(first_ten.begin(), first_ten.end()); // 只排序前10个元素

示例:C++20 范围算法直接接受视图

1
2
3
std::span<int> s = get_data();
auto result = std::ranges::find(s, 42);
if (result != s.end()) { ... }

6. 注意事项总结

  • 生命周期:永远确保视图的源数据在使用期间有效。
  • 不要存储视图:类成员尽量不要存储视图(除非你明确知道外部数据生命周期)。
  • 空终止:使用 string_viewdata() 调用 C 字符串函数时,如果视图不包含空字符,会导致越界。可先用 std::string 构造临时副本,或确保视图完整且含空终止(但视图通常不含)。
  • 迭代器失效:如果原始容器重新分配内存(如 vectorpush_back 导致扩容),现有视图会失效。
  • 隐式转换std::vector 等容器可以隐式转换为 span,但要注意 const 正确性。

结语

std::string_viewstd::span 是现代 C++ 中非拥有视图的重要工具,它们提升了代码的灵活性和安全性,同时避免了不必要的拷贝。理解它们的区别和适用场景,能帮助我们写出更高效、更清晰的代码。结合 STL 算法,视图类可以成为数据处理流水线中的关键一环。


现代 C++ 零拷贝视图体系详解 —— std::span 与 std::string_view 完整指南
https://www.psnow.sbs/2026/02/09/现代-C-零拷贝视图体系详解-——-std-span-与-std-string-view-完整指南/
作者
Psnow
发布于
2026年2月10日
许可协议