程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

c++视图类string_view和span的实现原理和性能优势

balukai 2024-12-31 09:14:06 文章精选 9 ℃

C++为了节省内存提升效率,在现代c++中,增加了一些视图类新语法,方便开发者使用。下面,我们就对常用的string_view和span的实现原理和性能优势进行详细介绍。

string_view的定义、应用场景及特点

定义

string_view是 C++17 引入的一个轻量级的只读字符串视图类。它提供了一种对字符串(通常是const char*指向的以空字符结尾的字符数组或者std::string对象)的非拥有性引用方式,即它本身并不拥有字符串的存储空间,只是指向已存在的字符串数据。

例如:

#include <string_view>
#include <iostream>

int main() {
    const char* str = "Hello, World!";
    std::string_view sv(str);

    std::cout << sv << std::endl;

    return 0;
}

在上述代码中,sv就是基于str创建的string_view,它可以像操作普通字符串一样进行读取等操作,但不会复制字符串内容。

应用场景

  • 函数参数传递:当函数只需要读取字符串内容而不需要拥有或修改它时,使用string_view作为参数类型可以避免不必要的字符串复制。比如:
void printString(std::string_view sv) {
    std::cout << sv << std::endl;
}

int main() {
    std::string str = "This is a test string";
    printString(str);

    const char* cstr = "Another test string";
    printString(cstr);

    return 0;
}

这样无论是std::string对象还是const char*类型的字符串都可以方便地传入函数,且函数内部不会进行额外的字符串复制操作,提高了性能。

  • 字符串比较和查找:在进行字符串比较(如判断是否相等、是否包含某个子串等)或者查找操作时,可以使用string_view来操作已有的字符串数据,而无需先将其转换为其他特定的字符串类型

特点

  • 高效:由于不复制字符串内容,在处理大量字符串数据或者频繁传递字符串参数的场景下,可以显著减少内存分配和复制的开销,提高程序运行效率。
  • 只读:它是只读的,不能通过string_view来修改所指向的字符串内容,保证了对原始字符串数据的安全性


span的定义、应用场景及特点

定义

span(在 C++20 中有了更完善的定义和支持,不过在一些较早的编译器中可能通过第三方库也能使用类似概念)同样是一种非拥有性的视图类型,但它更加通用,可以用于表示连续的对象序列,而不仅仅局限于字符序列(eg:string_view那样主要针对字符串)。

它本质上是对一块连续内存区域的引用,可以指定所引用内存区域的大小(元素个数)。例如,对于一个整数数组:

#include <span>
#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> sp(arr);

    for (int num : sp) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

这里的sp就是基于整数数组arr创建的span,它可以用来遍历数组元素等操作。

应用场景

  • 处理数组和容器元素序列:当需要对数组或者容器(如std::vector等)中的一段连续元素进行操作,且不想复制这些元素时,span就很有用。比如实现一个函数来计算数组中某一段元素的和:
#include <span>
#include <vector>
#include <iostream>

int sumSpan(std::span<int> sp) {
    int sum = 0;
    for (int num : sp) {
        sum += num;
    }
    return sum;
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::span<int> sp(vec.begin() + 2, vec.begin() + 6);

    int result = sumSpan(sp);
    std::cout << "The sum of the span elements is: " << result << std::endl;

    return 0;
}

在上述代码中,通过span可以方便地指定要计算和的那一段向量元素,而无需复制这些元素到新的数组或其他数据结构中

  • 与底层内存交互:在一些与底层内存打交道的场景,比如读取文件内容到内存缓冲区后,想要对缓冲区中的一段数据进行处理,就可以用span来表示和操作那一段内存数据。

特点

  • 通用性:不像string_view只针对字符串,span可以用于任何类型的连续对象序列,如整数数组、结构体数组等,提供了一种统一的处理连续内存区域元素的方式。
  • 灵活指定范围:可以通过指定起始位置和结束位置(或者起始位置和元素个数)来灵活地定义所引用的内存区域范围,方便对数组或容器内的不同部分进行操作。

string_view和span的应用区别

  • 数据类型针对性:
    • string_view主要是针对字符串数据,即const char*指向的以空字符结尾的字符数组或者std::string对象。它在字符串相关的操作场景下,如字符串的读取、比较、查找等,提供了高效的非拥有性引用方式。
    • span则更加通用,适用于任何类型的连续对象序列,不限于字符串。可以用于整数数组、结构体数组等各种类型的连续数据处理场景。
  • 操作语义:
    • string_view的操作语义主要围绕字符串的常见操作,比如获取字符串长度(通过size()等方法,这里的长度是指字符串中字符的个数,不包括末尾的空字符)、获取字符内容、进行字符串比较(如==、!=等比较运算符的重载)等,都是基于字符串的特性来设计的。
    • span的操作语义更侧重于对连续对象序列的遍历、访问单个元素、获取元素个数等通用操作,其操作方式不局限于某一种特定类型的数据(如字符串),而是适用于各种连续排列的对象。
  • 内存表示和处理:
    • string_view在内存中只需要存储指向字符串起始位置的指针以及字符串的长度(可能还有一些其他的辅助信息用于优化和正确处理字符串,比如是否以空字符结尾的标识等),因为它默认处理的是字符串这种特定的数据类型,其长度计算方式相对固定(以空字符结尾来确定长度等)。
    • span需要明确指定所引用的连续内存区域的范围,可以通过指定起始位置和结束位置或者起始位置和元素个数来确定。它对于不同类型的连续对象序列,需要根据具体对象的大小来准确确定所引用的内存区域范围,在内存表示上相对更灵活但也需要更精确的定义。

string_view和span的性能区别

string_view和span在性能方面有一些不同特点,具体区别如下:

内存开销相关性能

string_view

  • 内存占用:string_view通常只需要存储指向字符串起始位置的指针以及字符串的长度信息(可能还有少量用于优化和正确性处理的辅助数据,比如是否以空字符结尾的标识等)。它本身并不拥有字符串的存储空间,所以在内存占用上相对较小,尤其是在处理大量字符串时,不会因为复制字符串内容而导致额外的内存消耗。
  • 创建成本:创建string_view的成本很低,仅仅是记录下指向已有字符串的指针和相关长度信息,几乎没有额外的内存分配操作(除非有一些非常特殊的、用于内部优化的动态分配情况,但通常很少见)。例如:
const char* str = "Hello, World!"; 
std::string_view sv(str);

上述代码创建string_view sv时,只是简单地设置了指针和长度,没有涉及大量内存的分配。

span

  • 内存占用:span同样是一种非拥有性的视图,它存储的是指向连续内存区域起始位置的指针以及所引用内存区域的大小(元素个数或者通过起始和结束指针来确定范围)。对于简单类型的连续数组(比如int数组),其内存占用也相对较小,主要就是指针和表示范围的信息。但如果是处理复杂类型的连续序列(如结构体数组),且结构体本身较大,那么相对string_view来说,可能会因为要处理更大的元素而在存储表示范围等信息上有稍多一些的内存开销(不过总体还是比较小的)。
  • 创建成本:创建span的成本也较低,主要是设置指向连续内存区域的指针和确定所引用的范围。例如对于一个整数数组创建span:
int arr[] = {1, 2, 3, 4, 5};
std::span<int> sp(arr);

同样只是简单的指针设置和范围确定操作,没有大规模的内存分配。然而,如果要通过指定起始和结束指针来精确创建

span,可能需要额外的计算来确定元素个数等信息,但这种成本通常也不高。

数据访问相关性能

string_view

  • 字符串访问:当访问string_view所指向字符串中的字符时,由于它直接指向原始字符串数据,所以访问速度通常较快,就如同直接访问普通的const char*指向的字符串一样(实际上它内部很多操作就是基于对这个指针的操作)。例如:
std::string_view sv("Hello"); 
char firstChar = sv[0]; // 快速获取第一个字符

这里获取字符的操作就是简单的指针偏移运算,性能较好。

  • 字符串遍历:在遍历string_view所指向的字符串时,同样因为是直接基于指针的操作,只要处理器的缓存等机制能有效配合,遍历速度也比较可观。比如:
std::string_view sv("Hello, World!"); 
for (size_t i = 0; i < sv.size(); ++i)
{
  char ch = sv[i];
}

在这个遍历过程中,每次获取字符的操作开销相对较小。

span

  • 元素访问:对于span,当访问其中的元素时,其性能取决于所引用的连续对象序列的类型。如果是简单类型(如int数组),访问元素的速度也很快,类似直接访问数组元素,通过指针偏移运算来实现。例如:
int arr[] = {1, 2, 3, 4, 5}; 
std::span<int> sp(arr);
 int firstElement = sp[0]; // 快速获取第一个元素

但如果是复杂类型(如结构体数组),且结构体中有较多的数据成员,那么每次访问元素可能涉及到更多的内存读取操作(因为要获取整个结构体的内容),相对来说访问速度可能会比访问简单类型的数组稍慢一些。

  • 序列遍历:在遍历span所引用的连续对象序列时,同样受所引用对象类型的影响。对于简单类型数组,遍历速度可以很快,但对于复杂类型数组,由于每次遍历都要处理相对复杂的结构体等对象,可能会有稍多一些的开销,不过总体来说在合理的范围内,只要不是处理极其庞大且复杂的结构体数组,一般不会出现明显的性能瓶颈。

函数调用相关性能

string_view

  • 在函数调用中,如果将string_view作为参数传递,由于它不复制字符串内容,只是传递指针和长度信息,所以函数调用的开销很小。这在频繁调用函数且需要传递字符串参数的场景下,能够显著提高性能。例如:
void printString(std::string_view sv) {
    std::cout << sv << std::endl;
}

int main() {
    std::string str = "This is a test string";
    printString(str);

    return 0;
}

在这里,将std::string对象str以string_view的形式传递给printString函数,避免了字符串复制,使得函数调用更加高效。

span

  • 类似地,当将span作为参数传递给函数时,也只是传递指针和所引用范围的信息,不涉及复制所引用的连续对象序列,所以函数调用开销也较小。比如:
void processSpan(std::span<int> sp) {
    for (int num : sp) {
        // 处理元素
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::span<int> sp(arr);
    processSpan(sp);

    return 0;
}

在这个例子中,将span传递给processSpan函数,没有复制整数数组的元素,从而降低了函数调用的成本。

最近发表
标签列表