首页 > 编程笔记 > C++笔记 阅读:2

C++ std::span的用法(附带实例)

C++17 中,标准库中添加了 std::string_view 类型。这是一个对象,表示连续的常量字符序列的视图。视图通常使用指向序列第一个元素的指针和长度来实现。

字符串是所有编程语言中使用非常广的数据类型之一,具有不分配内存、避免复制的无所有权视图,并且具有比 std::string 更快的一些操作,这是一个重要的优势。然而,字符串只是一个特殊的字符 vector 数组,具有特定于文本的操作。因此,无论对象的类型是什么,拥有一个连续对象序列的视图类型是有意义的,这就是 C++20 中的 std::span 类模板所代表的内容。

我们可以说,std::span 之于 std::vector 和 array 数组类型,就像 std::string_view 之于 std::string 一样。

类模板 std::span 可在头文件 <span> 中找到。

C++ std::span的使用方式

优先使用 std::span<T>,而不是像类 C 接口那样使用成对的指针和大小。换句话说,应将以下函数:
void func(int* buffer, size_t length) { /* ... */ }
替换为:
void func(std::span<int> buffer) { /* ... */ }

当使用 std::span时,可以执行以下操作:
1) 通过指定 span 中元素的数量来创建具有编译时长度(称为静态范围)的 span:
int arr[] = {1, 1, 2, 3, 5, 8, 13};
std::span<int, 7> s {arr};

2) 通过不指定 span 中的元素数量来创建具有运行时长度(称为动态范围)的 span:
int arr[] = {1, 1, 2, 3, 5, 8, 13};
std::span<int> s {arr};

3) 在基于 range 的 for 循环中使用 span:
void func(std::span<int> buffer)
{
    for(auto const e : buffer)
        std::cout << e << ' ';
    std::cout << '\n';
}

4) 使用 front()、back()、data() 和 operator[] 方法访问 span 的元素:
int arr[] = {1, 1, 2, 3, 5, 8, 13};
std::span<int, 7> s {arr};
std::cout << s.front() << " == " << s[0] << '\n'; // 1 == 1
std::cout << s.back() << " == " << s[s.size() - 1] << '\n'; // 13 == 13

5) 使用 first()、last() 和 subspan() 方法获取 span 的子 span:
std::span<int> first_3 = s.first(3);
func(first_3); // 1 1 2
std::span<int> last_3 = s.last(3);
func(last_3); // 5 8 13
std::span<int> mid_3 = s.subspan(2, 3);
func(mid_3); // 2 3 5

C++ std::span的工作原理

类模板 std::span 并非对象的容器,而是定义连续对象序列视图的轻量级包装器。

最初,span 被称为 array_view,一些人认为 array_view 才是更好的名称,因为它清楚地表明该类型是序列的无所有权视图,而且它与 array_view 的名称是一致的。然而,该类型在标准库中以 span 的名称被采用。

虽然标准没有指定实现细节,但 span 通常是通过存储指向序列第一个元素的指针和表示视图中元素数量的长度来实现的。因此,span 可用于在 std::vector、std::array、T[] 或 T*(但不仅限于此)上定义无所有权视图。但是,它不能用于列表或关联容器(例如 std::list、std::map 或 std::set),因为这些不是连续元素序列的容器。

span 可以是编译时大小,也可以是运行时大小。当在编译时指定 span 中的元素数量时,就拥有了一个具有静态范围(编译时大小)的 span。如果没有指定元素的数量,而是在运行时确定元素数量,则我们有一个动态范围的 span。

std::span 类有一个简单的接口,主要由下表所示的成员组成:

成员函数 描述
begin(), end() 指向序列第一个元素和最后一个元素之后的可变迭代器和常量迭代器
cbegin(), cend() 指向序列第一个元素和最后一个元素之后的可变迭代器和常量迭代器
rbegin(), rend() 指向序列开头和结尾的可变反向迭代器和常量反向迭代器
crbegin(), crend() 指向序列开头和结尾的可变反向迭代器和常量反向迭代器
front(), back() 访问序列的第一个元素和最后一个元素
data() 返回一个指向元素序列开头的指针
operator[] 访问由其索引指定的序列的元素
size() 获取序列中元素的个数
size_bytes() 以字节为单位获取序列的大小
empty() 检查序列是否为空
first() 检索包含序列的前 N 个元素的子 span
last() 检索包含序列最后 N 个元素的子 span
subspan() 检索包含从指定偏移量开始的 N 个元素的子 span。如果未指定 N,则返回一个包含从偏移量到序列末尾的所有元素的 span

span 不能用于使用指向 range 开头和结尾的一对迭代器的通用算法(如 sort、copy、find_if 等),也不能用于替代标准容器。它的主要目的是构建比类 C接口更好的接口,后者将指针和大小传递给函数。用户可能会传递错误的大小值,从而导致访问超出序列界限的内存。

span 提供了安全性检查和边界检查,它也是将函数的 const 引用传递给 std::vector<T> (std::vector<T>const &)的一个很好的替代方法。span 不拥有元素,并且足够小,因此可以按值传递(不应该按引用或按常量引用传递 span)。

std::string_view 不支持改变序列中元素的值,与之不同的是,std::span 定义了一个可变视图,并支持修改其中的元素。为此,像 front()、back() 和 operator[] 这样的函数会返回一个引用。

相关文章