C++ `std::span`与C数组/STL容器的零拷贝视图:实现安全且高效的数据访问

C++ std::span:零拷贝视图,安全高效的数据访问

各位同学,大家好。今天我们来深入探讨C++20引入的一个非常重要的工具——std::spanstd::span提供了一种零拷贝的方式来访问和操作连续内存块,它可以作为C数组和STL容器的通用视图,极大地提高了代码的灵活性、安全性和效率。

1. std::span 简介

std::span本质上是一个非拥有(non-owning)的连续内存区域的视图。这意味着std::span本身不负责管理其指向的内存,它只是提供了一种访问和操作该内存的途径。 这带来了显著的优势:

  • 零拷贝: 创建std::span不会复制数据,只是创建一个指向现有数据的指针和一个长度。
  • 类型安全: std::span知道它所指向的数据的类型和大小,从而可以在编译时进行类型检查,防止越界访问等错误。
  • 通用性: std::span可以用于C数组、std::vectorstd::array等多种数据结构,提供一致的访问接口。
  • 性能: 由于零拷贝和类型安全,std::span通常能提供与直接使用指针相当的性能,同时避免了指针操作带来的风险。

2. std::span 的基本用法

std::span定义在 <span> 头文件中。它的模板定义如下:

template< class T, std::size_t Extent = std::dynamic_extent > class span;
  • Tspan引用的元素的类型。
  • Extentspan的长度。可以是编译时常量(静态extent)或运行时常量(动态extent)。

2.1 构造 std::span

std::span可以通过多种方式构造:

  • 从C数组构造:

    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> span_arr(arr, 5); // 动态extent
    std::span<int, 5> span_arr_static(arr); // 静态extent
    
    for (int i = 0; i < span_arr.size(); ++i) {
        std::cout << span_arr[i] << " ";
    }
    std::cout << std::endl;
  • std::vector 构造:

    std::vector<int> vec = {6, 7, 8, 9, 10};
    std::span<int> span_vec(vec); // 动态extent
    
    for (int i = 0; i < span_vec.size(); ++i) {
        std::cout << span_vec[i] << " ";
    }
    std::cout << std::endl;
  • std::array 构造:

    std::array<int, 5> arr_std = {11, 12, 13, 14, 15};
    std::span<int, 5> span_arr_std(arr_std); // 静态extent
    //std::span<int> span_arr_std(arr_std); // This is also valid
    
    for (int i = 0; i < span_arr_std.size(); ++i) {
        std::cout << span_arr_std[i] << " ";
    }
    std::cout << std::endl;
  • 从指针和长度构造:

    int* data = new int[5]{16,17,18,19,20};
    std::span<int> span_ptr(data, 5); // 动态extent
    
    for (int i = 0; i < span_ptr.size(); ++i) {
        std::cout << span_ptr[i] << " ";
    }
    std::cout << std::endl;
    
    delete[] data; // 重要:记得释放内存

2.2 std::span 的成员函数

std::span提供了一系列成员函数用于访问和操作数据:

函数 描述
size() 返回span的长度。
empty() 如果span为空,则返回true,否则返回false
data() 返回指向span所指向内存的指针。
front() 返回span的第一个元素的引用。
back() 返回span的最后一个元素的引用。
operator[] 访问span中指定索引的元素。
subspan() 创建一个新的span,它是原始span的一个子区域。
first() 创建一个新的span,包含原始span的前N个元素。
last() 创建一个新的span,包含原始span的后N个元素。

示例:使用 subspan()

std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::span<int> full_span(data);

// 创建一个包含从索引2开始的3个元素的子span
std::span<int> sub_span = full_span.subspan(2, 3);

for (int i = 0; i < sub_span.size(); ++i) {
    std::cout << sub_span[i] << " "; // 输出: 3 4 5
}
std::cout << std::endl;

// 创建一个包含从索引2到末尾的子span
std::span<int> sub_span_to_end = full_span.subspan(2);

for (int i = 0; i < sub_span_to_end.size(); ++i) {
    std::cout << sub_span_to_end[i] << " "; // 输出: 3 4 5 6 7 8 9 10
}
std::cout << std::endl;

// 创建一个包含前3个元素的子span
std::span<int> first_span = full_span.first(3);

for (int i = 0; i < first_span.size(); ++i) {
    std::cout << first_span[i] << " "; // 输出: 1 2 3
}
std::cout << std::endl;

// 创建一个包含后3个元素的子span
std::span<int> last_span = full_span.last(3);

for (int i = 0; i < last_span.size(); ++i) {
    std::cout << last_span[i] << " "; // 输出: 8 9 10
}
std::cout << std::endl;

3. 静态Extent vs 动态Extent

std::span的长度可以是静态的(编译时已知)或动态的(运行时已知)。

  • 静态Extent:

    • 长度在编译时确定。
    • 使用 std::span<T, N> 声明,其中 N 是一个编译时常量。
    • 优点:可以进行更多的编译时优化,例如,编译器可以内联对 size() 的调用。
    • 缺点:灵活性较差,只能用于长度固定的数据。
  • 动态Extent:

    • 长度在运行时确定。
    • 使用 std::span<T>std::span<T, std::dynamic_extent> 声明。
    • 优点:灵活性强,可以用于长度可变的数据。
    • 缺点:编译时优化可能较少。

示例:静态Extent vs 动态Extent

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

void print_static_span(std::span<int, 5> span) {
    std::cout << "Static span: ";
    for (int i = 0; i < span.size(); ++i) {
        std::cout << span[i] << " ";
    }
    std::cout << std::endl;
}

void print_dynamic_span(std::span<int> span) {
    std::cout << "Dynamic span: ";
    for (int i = 0; i < span.size(); ++i) {
        std::cout << span[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    std::vector<int> vec = {6, 7, 8, 9, 10, 11};

    std::span<int, 5> static_span(arr); // 静态Extent
    std::span<int> dynamic_span_arr(arr); // 动态Extent
    std::span<int> dynamic_span_vec(vec); // 动态Extent

    print_static_span(static_span);
    print_dynamic_span(dynamic_span_arr);
    print_dynamic_span(dynamic_span_vec);

    return 0;
}

4. std::span 与 const

std::span可以指向可修改的或只读的数据。

  • std::span<int>:指向可修改的 int 数据的 span
  • std::span<const int>:指向只读的 int 数据的 span

示例:std::span 与 const

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

void modify_span(std::span<int> span) {
    if (!span.empty()) {
        span[0] = 100; // 可以修改span指向的数据
    }
}

void print_span(std::span<const int> span) {
    std::cout << "Span: ";
    for (int i = 0; i < span.size(); ++i) {
        std::cout << span[i] << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5};
    std::span<int> mutable_span(data);
    std::span<const int> const_span(data);

    print_span(const_span); // 输出: Span: 1 2 3 4 5
    modify_span(mutable_span);
    print_span(const_span); // 输出: Span: 100 2 3 4 5 (数据被修改了)

    // 无法通过 const_span 修改数据
    // const_span[0] = 200; // 编译错误!

    return 0;
}

5. std::span 的优势

  • 安全性: std::span 提供了类型安全和边界检查,减少了指针操作带来的风险。使用std::span可以避免悬挂指针和缓冲区溢出等问题。
  • 效率: std::span 是零拷贝的,避免了不必要的数据复制,提高了性能。
  • 通用性: std::span 可以用于多种数据结构,提供一致的访问接口,提高了代码的复用性。
  • 代码可读性: 使用 std::span 可以更清晰地表达代码的意图,提高代码的可读性和可维护性。

6. std::span 的应用场景

  • 函数参数传递: 使用 std::span 作为函数参数,可以接受 C 数组、std::vectorstd::array 等多种类型的数据,提高了函数的通用性。

    void process_data(std::span<int> data) {
        // 处理数据
        for (int i = 0; i < data.size(); ++i) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    }
    
    int main() {
        int arr[] = {1, 2, 3};
        std::vector<int> vec = {4, 5, 6};
    
        process_data(arr); // 可以传递 C 数组
        process_data(vec); // 可以传递 std::vector
        return 0;
    }
  • 算法实现: 在算法实现中使用 std::span 可以提高算法的通用性,使其可以处理多种类型的数据。

    int sum(std::span<const int> data) {
        int result = 0;
        for (int value : data) {
            result += value;
        }
        return result;
    }
    
    int main() {
        int arr[] = {1, 2, 3, 4, 5};
        std::vector<int> vec = {6, 7, 8, 9, 10};
    
        std::cout << "Sum of array: " << sum(arr) << std::endl;
        std::cout << "Sum of vector: " << sum(vec) << std::endl;
    
        return 0;
    }
  • 库的设计: 在库的设计中使用 std::span 可以提供更灵活和安全的接口。

7. std::spanstd::string_view 的比较

std::string_viewstd::span 都是 C++17 引入的非拥有视图,但它们的应用场景不同:

  • std::string_view:专门用于字符串(charwchar_t 数组)的只读视图。它假设数据是 null 结尾的。
  • std::span:用于任意连续内存区域的视图,不要求 null 结尾。它可以是可修改的或只读的。

8. 注意事项

  • std::span 不拥有它所指向的内存,因此在使用 std::span 时,必须确保它所指向的内存的生命周期长于 std::span 本身的生命周期。否则,会导致悬挂指针的问题。
  • 在使用 std::span 时,要特别注意边界检查,避免越界访问。虽然 std::span 提供了一些边界检查,但仍然需要自己进行额外的验证。
  • 当使用从指针构造 std::span 时,尤其要保证指针的有效性, 并在不再使用指针指向的内存后,释放内存。

9. 总结:std::span 提升代码质量

std::span 是一个强大的工具,它提供了一种安全、高效和通用的方式来访问和操作连续内存块。 通过学习和使用std::span,我们可以编写出更健壮、更灵活和更易于维护的C++代码。 在函数参数传递和算法实现等场景中,std::span 都能够发挥重要作用,提升代码质量。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注