C++ span

在 C++20 中,std::span 是 <span> 头文件提供的 “连续序列视图”,用来在不复制底层元素的情况下统一观察和操作一段连续排列的数据。

它的设计思路和前面学过的 std::string_view 非常相似,只不过 std::span 面向的是任意类型的连续序列。像原生数组、std::array 和 std::vector,它们的元素在内存里都是紧挨着存放的,都可以直接交给 std::span 处理。

C++ span 的定义与读取

在 C++ 中,std::span 本身并不开辟新内存去复制元素,它内部只记录了两个轻量信息:① 序列首元素的内存地址;② 元素的总个数。

语法:

#include <span>

std::span<元素类型> 变量名 = 连续序列;

说明:

定义 std::span 时,我们在尖括号里写上元素的类型即可。与 std::string_view 默认只读不同,普通的 std::span<T> 是 “可读可写” 的,通过它不仅能读取元素,还能直接修改原序列里的值。

如果我们希望视图只能读取、不允许修改原数据,把元素类型写成带 const 的 std::span<const T> 就可以了。

示例 1:观察不同连续序列

#include <array>
#include <iostream>
#include <span>
#include <vector>

int main() {
    int rawArr[3] = {10, 20, 30};
    std::array<int, 4> fixedArr = {40, 50, 60, 70};
    std::vector<int> dynVec = {80, 90, 100, 110, 120};

    std::span<int> s1 = rawArr;
    std::span<int> s2 = fixedArr;
    std::span<int> s3 = dynVec;

    std::cout << "首元素:" << s1[0] << ", " << s2[0] << ", " << s3[0] << '\n';
    std::cout << "s1 长度:" << s1.size() << '\n';
    std::cout << "s2 长度:" << s2.size() << '\n';
    std::cout << "s3 长度:" << s3.size() << '\n';
    return 0;
}

运行结果如下。

首元素:10, 40, 80
s1 长度:3
s2 长度:4
s3 长度:5

分析:

上面代码中,rawArr 是长度为 3 的原生数组,fixedArr 是长度为 4 的 std::array,dynVec 则是包含 5 个元素的 std::vector。

虽然这三者的具体类型各不相同,但通过 std::span<int> 建立视图之后,都能自动识别出各自的元素个数。我们可以统一使用下标 [] 访问元素,也可以调用 size() 获取长度、调用 empty() 判断序列是否为空。

注意 std::span 只能用于元素在内存中 “连续排列” 的序列,比如原生数组、std::array 和 std::vector。而像 std::list、std::set、std::map 这类容器,元素分散在不同的内存节点上,并不是紧挨着存放的,因此不能转换成 std::span。

使用 span 作为函数参数

在实际开发中,std::span 最常用的场景就是充当函数的形参。在没有 std::span 之前,如果我们要编写一个函数来处理一组整数,通常会遇到两难选择:

  • 传指针加长度:比如把形参写成 int* arr, int size。这种方式虽然兼容原生数组,但每次调用时我们都要手动传长度,一旦传错就容易引发越界访问。
  • 传容器引用:比如把形参写成 const std::vector<int>&。这种方式虽然自带长度信息,但如果调用者手里是一个原生数组或 std::array,就没法直接传进去了。

现在我们只要把函数形参写成 std::span,上面两个问题就同时解决了。同一个函数既自带长度信息、支持直接使用范围 for 循环遍历,又能同时接收这 3 种连续序列。下面我们来看一个例子。

示例 2:作为可写与只读形参

#include <array>
#include <iostream>
#include <span>
#include <vector>

// 可写视图
void addTen(std::span<int> values) {
    for (int& val : values) {
        val += 10;
    }
}

// 只读视图
void printItems(std::span<const int> values) {
    for (int val : values) {
        std::cout << val << ' ';
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> nums = {10, 20, 30};
    std::array<int, 3> fixedArr = {40, 50, 60};
    int scores[2] = {80, 90};

    addTen(nums);
    printItems(nums);        // 传入 std::vector
    printItems(fixedArr);    // 传入 std::array
    printItems(scores);      // 传入原生数组
    return 0;
}

运行结果如下。

20 30 40
40 50 60
80 90

分析:

在这个例子中,addTen() 的形参是可写视图 std::span<int>。因为视图直接指向 nums 底层的那块连续内存,所以在循环里给每个元素加 10 之后,原来的 nums 也直接变成了 20、30、40。

而 printItems() 只需要读取数据,因此我们把形参写成了只读视图 std::span<const int>,既能防止误改原数据,又能统一接收 std::vector、std::array 和原生数组。

此外小伙伴们要注意,一个 std::span 对象内部只保存了一个指针和一个长度整数,在 64 位系统上只占 16 个字节。因为它本身非常轻便,作为函数形参时我们直接按值传递即可,不需要额外加上 & 引用符号。

截取子视图:first()、last() 与 subspan()

有时候我们并不需要处理整个序列,而是只想操作其中的某一部分,比如只读取最前面的 2 个元素,或者只修改中间的一段元素。在 std::span 中,常用的子视图截取方法有以下 3 个:

  • first(count):截取最前面的 count 个元素,生成并返回一个子视图。
  • last(count):截取最后面的 count 个元素,生成并返回一个子视图。
  • subspan(offset, count):从下标 offset 开始,截取 count 个元素生成子视图。如果省略第 2 个参数 count,则默认一直截取到末尾。

示例 3:截取子视图并修改元素

#include <iostream>
#include <span>
#include <vector>

int main() {
    std::vector<int> nums = {10, 20, 30, 40, 50};
    std::span<int> all = nums;

    std::span<int> head = all.first(2);        // 前 2 个元素
    std::span<int> tail = all.last(2);         // 后 2 个元素
    std::span<int> mid = all.subspan(1, 3);    // 中间 3 个元素

    std::cout << "head 首元素:" << head[0] << '\n';
    std::cout << "tail 末元素:" << tail[1] << '\n';

    for (int& val : mid) {
        val += 100;
    }

    std::cout << "修改后原容器:";
    for (int val : nums) {
        std::cout << val << ' ';
    }
    std::cout << '\n';
    return 0;
}

运行结果如下。

head 首元素:10
tail 末元素:50
修改后原容器:10 120 130 140 50

分析:

all.subspan(1, 3) 这句代码表示从下标 1 开始截取 3 个元素,也就是 20、30、40。由于 subspan() 同样不会复制数据,子视图 mid 指向的依然是原容器 nums 里的元素。因此我们遍历 mid 给每个元素加 100 后,nums 中间的 3 个数就直接变成了 120、130、140。

正因为 std::span 不保管底层数据,小伙伴们在使用时要避开 2 种常见的视图失效情况。第 1 种情况是在函数里返回局部数组的视图,函数执行结束时局部数组就会销毁,返回的视图也会像悬空指针一样失效:

std::span<int> getBadSpan() {
    int localArr[3] = {1, 2, 3};
    return localArr;    // 局部数组即将销毁
}

第 2 种情况是在持有视图期间,向 std::vector 添加元素触发了扩容。一旦容量不足,容器就会重新申请新内存、搬移元素并释放旧内存,指着旧地址的视图就会跟着失效:

std::vector<int> nums = {10, 20, 30};
std::span<int> view = nums;

nums.push_back(40);              // 可能触发扩容
std::cout << view[0] << '\n';    // 旧视图已失效

如果我们在程序运行过程中确实向 std::vector 添加或删除了元素,只需要在修改完容器大小之后,重新创建一个新的 std::span 再去访问即可。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号