3.4、std::span视图

什么是std::span

std::span本质上是一个轻量级的非所有权视图,用于表示一段连续内存区域中的对象序列。它定义在<span>头文件中,设计目标是替代传统的指针+长度组合,提供更安全、更富表现力的接口。

简单来说,std::span是:

  • • 一个非拥有性视图:它只是对已有数据的引用,不负责管理这些数据的生命周期
  • • 只能引用连续序列:如数组、std::vectorstd::array中的元素
  • • 可以具有静态范围(编译时已知长度)或动态范围(运行时确定长度)
  • • 一个零开销抽象:编译器能对其进行优化,消除不必要的开销

为什么我们需要span

C语言留给我们的最大遗产之一就是数组退化为指针的问题。当数组作为函数参数传递时,关于其大小的信息丢失了,导致了无数的安全漏洞和程序错误。

思考这个典型的C风格函数:

void process_array(int* arr, size_t length);

这种设计存在几个明显问题:

  • • 编译器无法检查传入的length是否与实际数组大小匹配
  • • 调用时必须分别传递数组和大小,容易出错
  • • 无法在使用时区分是单个元素的指针还是数组的开始

std::span优雅地解决了这些问题,提供了一种安全且高效的方式来引用连续内存区域。

基本用法详解

创建span

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

int main() {
    // 从原始数组创建
    int arr[] = {12345};
    std::span<int> sp1{arr};  // 动态范围,大小在运行时确定
    
    // 从vector创建
    std::vector<int> vec{1020304050};
    std::span<int> sp2{vec};
    
    // 从数组部分区间创建
    std::span<int> sp3{arr, 3};  // 只包含前3个元素
    
    // 创建静态范围的span(编译时已知大小)
    std::span<int5> sp4{arr};  // 必须正好有5个元素

    // 打印span大小
    std::cout << "sp1 size: " << sp1.size() << std::endl;
    std::cout << "sp2 size: " << sp2.size() << std::endl;
    std::cout << "sp3 size: " << sp3.size() << std::endl;
    std::cout << "sp4 size: " << sp4.size() << std::endl;
}

常用操作

std::span提供了丰富的接口来操作内存区域:

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

void print_span(std::span<const int> sp) {
    for (const auto& elem : sp) {
        std::cout << elem << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> vec{1020304050};
    std::span<int> sp{vec};
    
    // 元素访问
    std::cout << "第一个元素: " << sp.front() << std::endl;
    std::cout << "最后一个元素: " << sp.back() << std::endl;
    std::cout << "第三个元素: " << sp[2] << std::endl;
    
    // 检查是否为空
    std::cout << "span 是否为空: " << std::boolalpha << sp.empty() << std::endl;
    
    // 创建子视图
    auto first3 = sp.first(3);  // 前3个元素
    auto last3 = sp.last(3);    // 后3个元素
    auto mid3 = sp.subspan(13);  // 从索引1开始的3个元素
    
    std::cout << "前3个元素: ";
    print_span(first3);
    
    std::cout << "后3个元素: ";
    print_span(last3);
    
    std::cout << "中间3个元素: ";
    print_span(mid3);
    
    // 修改元素(原始数据也会改变)
    sp[0] = 100;
    std::cout << "修改后的vector: " << vec[0] << std::endl;
}

深入解析与高级用法

静态范围vs动态范围

std::span有两种类型:固定大小(静态范围)和可变大小(动态范围)。

template<typename ElementType, size_t Extent = dynamic_extent>
class span;

静态范围的span在编译时就知道其大小,可以让编译器进行更多优化。当你确切知道视图大小时,应优先使用静态范围:

std::array<int, 5> arr{12345};

// 编译时确定大小的span
std::span<int5> sp1{arr};  // 编译器知道有5个元素

// 如果尝试创建不匹配的静态span,会产生编译错误
// std::span<int, 4> sp2{arr};  // 错误:尺寸不匹配

函数参数中的span

std::span在函数参数中特别有用,它避免了不必要的拷贝,同时提供了更安全的接口:

// 过去的方式(危险且容易出错)
void process_data_old(int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        data[i] *= 2;
    }
}

// 使用span的现代方式(安全且富有表现力)
void process_data_new(std::span<int> data) {
    for (auto& elem : data) {
        elem *= 2;
    }
}

int main() {
    std::vector<int> vec{12345};
    int arr[5] = {12345};
    
    // 旧方式(必须手动传递大小,容易出错)
    process_data_old(vec.data(), vec.size());
    process_data_old(arr, 5);
    
    // 新方式(自动推导大小,统一接口)
    process_data_new(vec);
    process_data_new(arr);
}

模板与span结合的高级用法

当你需要编写能处理任意大小的固定数组的函数时,std::span与模板结合使用非常强大:

template<typename T, std::size_t N>
void process_fixed_array(std::span<T, N> data) {
    // 在编译时知道确切大小,可以启用更多优化
    static_assert(N > 0"Cannot process empty arrays");
    
    // 可以安全地访问最后一个元素,因为编译器知道N > 0
    T last = data[N-1];
    
    // 处理数据...
}

int main() {
    int arr5[5] = {12345};
    int arr10[10] = {12345678910};
    
    // 编译器会为每种不同大小的数组生成不同的函数实例
    process_fixed_array(std::span{arr5});   // 实例化 process_fixed_array<int, 5>
    process_fixed_array(std::span{arr10});  // 实例化 process_fixed_array<int, 10>
}

常见错误与陷阱

使用std::span时需要特别注意几个常见错误:

  1. 1. 引用临时对象
    最危险的错误之一是创建引用临时对象的span
// 危险:getData()返回一个临时vector,span引用的内存很快会被释放
std::vector<intgetData() return {12345}; }

void dangerous() {
    std::span<int> sp{getData()};  // 严重错误:引用了临时对象
    // sp中的指针现在指向已释放的内存
    std::cout << sp[0];  // 未定义行为
}
  1. 2. 忘记维护被引用对象的生命周期
    std::span是非拥有性视图,它不控制底层数据的生命周期:
std::span<intcreate_span() {
    std::vector<int> vec{123};  // 局部变量
    return std::span{vec};  // 错误:返回引用局部变量的span
}  // vec在这里被销毁

void use_span() {
    auto sp = create_span();  // sp现在引用无效内存
    std::cout << sp[0];  // 未定义行为
}
  1. 3. 忽略const修饰符
    即使span本身是const的,它引用的元素可能不是const的:
void process(const std::span<int> sp) {
    sp[0] = 42;  // 合法!虽然span是const的,但元素不是
}

// 如果想防止修改元素,应使用:
void process_readonly(std::span<const int> sp) {
    // sp[0] = 42;  // 错误:不能修改const元素
}

面试可能出现的问题

关于std::span可能出现以下问题:

  • • 问题std::span与传统指针+长度方法相比有什么优势?
    答案std::span提供类型安全,支持范围检查,简化接口,使代码意图更明确,支持范围for循环,提供丰富的成员函数,且在静态范围情况下能启用编译时优化。
  • • 问题std::span是按值传递还是按引用传递更好?
    答案std::span推荐按值传递。它内部只包含一个指针和一个大小(对于动态范围),复制代价很小。按值传递能保持语义一致性,且避免引用的引用的复杂性。
  • • 问题std::span<T>std::span<const T>有什么区别?
    答案std::span<T>允许修改被引用的元素,而std::span<const T>只允许读取元素,不能修改。这类似于T*const T*的区别。
  • • 问题:如何安全地从函数返回std::span
    答案:返回std::span时必须确保它引用的数据生命周期长于span本身。通常应避免返回引用局部变量的span,而应返回引用全局数据、静态数据或传入参数的子范围的span

总结

std::span填补了C++中长期缺失的"视图"概念,为我们提供了一种既安全又高效的方式来处理内存区域,而不必担心所有权问题。这种非拥有性视图的概念已经在C++的其他部分(如string_view)中证明了其价值,而std::span将这一概念扩展到了任意类型的连续序列。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)

 

阅读剩余
THE END