C++20之std::span:高效访问容器的神器

目录

1.概述

2.特点

3.用法

3.1.构造函数

3.2.迭代器

3.3.元素访问

3.3.1.front()

3.3.2.back()

3.3.3.operator[]

3.3.4.data()

3.4.观察器

3.5.子视图

3.5.1.first(N)

3.5.2.last(N)

3.5.3.subspan(offset, count)

4.注意事项

5.std::string_view与std::span的区别

6.总结


1.概述

        C++20引入了std::span作为一种语法糖,用于表示连续内存范围。它提供了一种轻量级的、非拥有式的、零开销的方式来引用数组或其他连续内存块。std::span可以用于传递数组片段给函数,或者在函数内部对连续内存进行操作,而无需进行内存拷贝。

        std::span提供连续对象序列的轻量级视图。 span 提供了一种安全的方法来对在内存中背靠背排列的对象进行迭代和索引。 例如存储在内置数组中的对象 std::array 或 std::vector。如果你通常使用指针和索引访问一系列背靠背对象,则 span 是一种更安全、轻量级的替代方案。要设置 span 的大小,可以在编译时通过将大小指定为模板参数,也可以在运行时指定 dynamic_extent。

2.特点

  • 轻量级std::span 的实现非常轻量,因为它不存储数据,只是存储了指向数据的指针和元素的数量。
  • 非拥有:与 std::vectorstd::array 等容器不同,std::span 不拥有它所引用的数据,这意味着当 std::span 被销毁时,它所引用的数据不会受到影响。
  • 类型安全std::span 提供了类型安全的接口来访问和操作其引用的元素。
  • 灵活性:它可以用来引用数组、std::arraystd::vector 或其他连续存储的容器的一部分或全部。

3.用法

3.1.构造函数

它的构造函数如下:

1)构造一个空的std::span,如:

std::span<int> numbers;

2)构造一个范围 【firstfirst + count)上的视图的 std::span,这个范围一般都是一个头部指针和长度,如:

void testCharSpan(const char* p, int len)
{
	std::span<const char>s6(p, 5);
}

//[1]
char test[] = "352352352346343463";
testCharSpan(test, sizeof(test));

//[2]
const char* p = "32521352356858568";
std::span<const char>s7(p, 10);

//[3]
std::vector<int>vec1{ 1, 2, 3, 4, 5, 6 };
std::span<int>s5(vec1.begin(), 5);

3)从迭代器的begin和end来构造std::span,如:

void show(std::span<int> arr) {
    for (auto&& elem : arr) {
        std::cout << elem << ' ';
    }
    std::cout << std::endl;
}

// std::vector
std::vector vec{111, 222, 333, 444};
show(vec);                        // 直接传vector
show({vec.begin(), vec.end()});   // 首尾迭代器
show({vec.begin(), vec.size()});  // 首迭代器+长度

4)  从C原生数组构造std::span,如:

void show(std::span<int> arr) {
    for (auto&& elem : arr) {
        std::cout << elem << ' ';
    }
    std::cout << std::endl;
}

int c_arr[] = {1, 2, 3, 4};
show(c_arr);                      // 直接传数组
show({c_arr, std::size(c_arr)});  // 传递指针+长度

5)从std::initializer_list构造std::span,如:

C++之std::initializer_list详解_c++ std initializer-CSDN博客

std::span<const int>s8({ 3,6,7,8,9,0,0 });

6)从一个std::span构造另外一个std::span, 如:

std::span<const int>s8({ 33,6,447,8,54549,0,0 });

std::span<const int>s9(s8);

std::span<const int>s10(s9);

3.2.迭代器

std::span给我们提供了正向的迭代器begin()、cbegin()、end()和cend(); 反向的迭代器rbegin()、crbegin()、rend()和crend(),示例如下:

#include <iostream>
#include <span>
 
void print(std::span<const int> array)
{
    std::cout << "array = ";
    for (auto it = array.begin(); it != array.end(); ++it)
        std::cout << *it << ' ';
    std::cout << '\n';
}
 
void set_first_element(std::span<int> sp, int new_value)
{
    if (!sp.empty())
    {
        std::cout << "old *begin = " << *sp.begin() << '\n';
        *sp.begin() = new_value;
        std::cout << "new *begin = " << *sp.begin() << '\n';
    }
}
 
int main()
{
    int array[]{1, 3, 4, 5};
    print(array);
    set_first_element(array, 2);
    print(array);
}

输出:

array = 1 3 4 5
old *begin = 1
new *begin = 2
array = 2 3 4 5

3.3.元素访问

3.3.1.front()

返回到 span 中首元素的引用。示例如下:

#include <iostream>
#include <span>
 
void print(std::span<const int> const data)
{
    for (auto offset{0U}; offset != data.size(); ++offset)
        std::cout << data.subspan(offset).front() << ' ';
    std::cout << '\n';
}
 
int main()
{
    constexpr int data[]{0, 1, 2, 3, 4, 5, 6};
    print({data, 4});
}

3.3.2.back()

返回到 span 中末元素的引用。示例如下:

#include <iostream>
#include <span>
 
void print_forward(std::span<const int> const span)
{
    for (auto n{span.size()}; n != 0; --n)
        std::cout << span.last(n).front() << ' ';
    std::cout << '\n';
}
 
void print_backward(std::span<const int> const span)
{
    for (auto n{span.size()}; n != 0; --n)
        std::cout << span.first(n).back() << ' ';
    std::cout << '\n';
}
 
int main()
{
    constexpr int numbers[]{0, 1, 2, 3, 4};
    print_forward(numbers);
    print_backward(numbers);
}

3.3.3.operator[]

获得到序列的第 idx 个元素的引用。若 idx 在范围外(即若它大于或等于 size())则行为未定义。示例如下:

#include <cstddef>
#include <iostream>
#include <span>
#include <utility>
 
void reverse(std::span<int> span)
{
    for (std::size_t i = 0, j = std::size(span); i < j; ++i)
    {
        --j;
        std::swap(span[i], span[j]);
    }
}
 
void print(std::span<const int> const span)
{
    for (int element : span)
        std::cout << element << ' ';
    std::cout << '\n';
}
 
int main()
{
    int data[]{1, 2, 3, 4, 5};
    print(data);
    reverse(data);
    print(data);
}

3.3.4.data()

返回指向序列起始的指针。示例如下:

#include <iostream>
#include <span>
 
int main()
{
    constexpr char str[] = "ABCDEF\n";
 
    const std::span sp{str};
 
    for (auto n{sp.size()}; n != 2; --n)
        std::cout << sp.last(n).data();
}

3.4.观察器

size() : 返回 span 中的元素数。

empty() : 检查 span 是否为空。

size_bytes() : 返回以字节计的序列大小。示例如下:

#include <cstdint>
#include <span>
 
int main()
{
    constexpr static std::int32_t a[]{1, 2, 3, 4, 5};
    constexpr static std::span s{a};
 
    static_assert
    (
        sizeof(int32_t) == 4 &&
        std::size(a) == 5 &&
        sizeof a == 20 &&
        s.size() == 5 &&
        s.size_bytes() == 20
    );
}

3.5.子视图

3.5.1.first(N)

获得由序列前 N 个元素组成的子段,示例如下:

#include <iostream>
#include <ranges>
#include <span>
#include <string_view>
 
void print(std::string_view const title,
           std::ranges::forward_range auto const& container)
{
    auto size{std::size(container)};
    std::cout << title << '[' << size << "]{";
    for (auto const& elem : container)
        std::cout << elem << (--size ? ", " : "");
    std::cout << "};\n";
}
 
void run_game(std::span<const int> span)
{
    print("span: ", span);
 
    std::span<const int, 5> span_first = span.first<5>();
    print("span.first<5>(): ", span_first);
 
    std::span<const int, std::dynamic_extent> span_first_dynamic = span.first(4);
    print("span.first(4): ", span_first_dynamic);
}
 
int main()
{
    int a[8]{1, 2, 3, 4, 5, 6, 7, 8};
    print("int a", a);
    run_game(a);
}

输出:

int a[8]{1, 2, 3, 4, 5, 6, 7, 8};
span: [8]{1, 2, 3, 4, 5, 6, 7, 8};
span.first<5>(): [5]{1, 2, 3, 4, 5};
span.first(4): [4]{1, 2, 3, 4};

3.5.2.last(N)

获得由序列末 N 个元素组成的子段, 示例如下:

#include <iostream>
#include <span>
#include <string_view>
 
void println(std::string_view const title, auto const& container)
{
    std::cout << title << '[' << std::size(container) << "]{ ";
    for (auto const& elem : container)
        std::cout << elem << ", ";
    std::cout << "};\n";
};
 
void run(std::span<const int> span)
{
    println("span: ", span);
 
    std::span<const int, 3> span_last = span.last<3>();
    println("span.last<3>(): ", span_last);
 
    std::span<const int, std::dynamic_extent> span_last_dynamic = span.last(2);
    println("span.last(2): ", span_last_dynamic);
}
 
int main()
{
    int a[8]{1, 2, 3, 4, 5, 6, 7, 8};
    println("int a", a);
    run(a);
}

输出:

int a[8]{ 1, 2, 3, 4, 5, 6, 7, 8, };
span: [8]{ 1, 2, 3, 4, 5, 6, 7, 8, };
span.last<3>(): [3]{ 6, 7, 8, };
span.last(2): [2]{ 7, 8, };

3.5.3.subspan(offset, count)

获取某个范围的子span, 示例如下:

#include <algorithm>
#include <cstdio>
#include <numeric>
#include <ranges>
#include <span>
 
void display(std::span<const char> abc)
{
    const auto columns{20U};
    const auto rows{abc.size() - columns + 1};
 
    for (auto offset{0U}; offset < rows; ++offset)
    {
        std::ranges::for_each(abc.subspan(offset, columns), std::putchar);
        std::putchar('\n');
    }
}
 
int main()
{
    char abc[26];
    std::iota(std::begin(abc), std::end(abc), 'A');
    display(abc);
}

输出:

ABCDEFGHIJKLMNOPQRST
BCDEFGHIJKLMNOPQRSTU
CDEFGHIJKLMNOPQRSTUV
DEFGHIJKLMNOPQRSTUVW
EFGHIJKLMNOPQRSTUVWX
FGHIJKLMNOPQRSTUVWXY
GHIJKLMNOPQRSTUVWXYZ

4.注意事项

  • 当使用 std::span 引用动态分配的内存(如 new[] 分配的内存)时,需要确保 std::span 的生命周期不会超过它所引用的内存的生命周期。
  • std::span 的设计初衷是为了提高代码的可读性和安全性,特别是在处理与原始数组和容器相关的算法和函数时。
  • 在某些情况下,可能需要谨慎使用 std::span,以确保不会意外地修改原始数据或超出其边界。
  • std::span不能用于不连续的序列,如:std::list, std::deque等。

5.std::string_view与std::span的区别

为什么C++17要引入std::string_view?_c++为何 要加 std-CSDN博客

        上面的博客详细讲解了std::string_view的用法,这一节又详细讲解了std::span,那它们之间有什么区别呢?

        std::string_view是C++17引入的一个轻量级类,用于表示对字符串的非拥有引用。它允许开发者以高效的方式传递和操作字符串,而无需复制或分配内存。std::string_view本质上是一个指向字符数组的指针和长度的组合,因此它可以引用任何以空字符结尾的字符数组(C风格字符串)、std::string或其他任何符合要求的字符序列。

        std::string_view仅用于观察字符串,不提供字符串修改功能。它非常适合用于函数参数,以减少不必要的字符串复制。

        std::span是一个模板类,用于表示对连续数据序列的非拥有引用。与std::string_view类似,std::span也不拥有其引用的数据,但它更为通用,可以引用任何类型的数据序列,而不仅仅是字符串。这使得std::span在处理数组、向量或其他连续数据结构时非常有用。

        std::span提供了一种类型安全的方式来引用和操作连续的数据序列。它可以用于任何类型的数据,并且支持随机访问和范围迭代。

        下面就从几个方面总结它们之间的差异:

1)类型特定性:std::string_view专门针对字符串数据,而std::span则更为通用,可以处理任何类型的数据序列。

2)操作限制:由于std::string_view专注于字符串,因此它提供了一些与字符串处理相关的成员函数(如substr、find等)。相比之下,std::span没有这些特定的字符串操作函数,但它提供了对底层数据的随机访问和迭代能力。

3)安全性:在使用std::span时,开发者需要明确指定所引用的数据类型,这增加了类型安全性。而std::string_view则隐式地假设底层数据是字符类型的序列。

4)用途:std::string_view主要用于字符串的传递和观察,特别适用于那些需要高效处理字符串且不希望产生额外内存分配的场景(如日志记录、字符串解析等)。而std::span则更适用于处理任意类型的数据序列,特别是在泛型编程和算法中非常有用。

6.总结

  std::span 是 C++20 引入的一个强大工具,它提供了一种灵活且类型安全的方式来引用和操作容器或数组的连续部分。通过减少不必要的数据复制和提供更清晰的代码接口,std::span 可以帮助编写更高效、更安全的 C++ 代码。

std::span - cppreference.com

  • 63
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 35
    评论
评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值