【现代C++】性能控的工具箱之string_view

栏目: C++ · 发布时间: 6年前

内容简介:本篇文章从在日常C/C++编程中,我们常进行数据的传递操作,比如,将数据传给函数。当数据占用的内存较大时,减少数据的拷贝可以有效提高程序的性能。在在

本篇文章从 string_view 引入的背景出发,依次介绍了其相关的知识点及使用方式,然后对常见的使用陷阱进行了说明,最后对该类型做总结。

一、背景

在日常C/C++编程中,我们常进行数据的传递操作,比如,将数据传给函数。当数据占用的内存较大时,减少数据的拷贝可以有效提高程序的性能。在 C 中指针是完成这一目的的标准数据结构,而 C++ 引入了安全性更高的引用类型。所以在 C++ 中若传递的数据仅仅只读, const string& 成了 C++ 的天然的方式。但这并非完美,从实践来看,它至少有以下几方面问题:

  1. 字符串字面值、字符数组、字符串指针的传递仍要数据拷贝
    这三类低级数据类型与 string 类型不同,传入时,编译器需要做隐式转换,即需要拷贝这些数据生成 string 临时对象。 const string& 指向的实际上是这个临时对象。通常字符串字面值较小,性能损耗可以忽略不计;但字符串指针和字符数组某些情况下可能会比较大(比如读取文件的内容),此时会引起频繁的内存分配和数据拷贝,会严重影响程序的性能。
  2. substr O(n) 复杂度
    这是一个特别常用的函数,好在 std::string 提供了这个函数,美中不足的是其每次都返回一个新生成的子串,很容易引起性能热点。实际上我们本意并不是要改变原字符串,为什么不在原字符串基础上返回呢?

C++17 中引入了 string_view ,能很好的解决以上两个问题。

二、std::string_view

从名字出发,我们可以类比数据库视图, view 表示该类型不会为数据分配存储空间,而且该数据类型只能用来读。该数据类型可通过 {数据的起始指针,数据的长度} 两个元素表示,实际上该数据类型的实例不会具体存储原数据,仅仅存储指向的数据的起始指针和长度,所以这个开销是非常小的。

要使用字符串视图,需要引入 <string_view> ,下面介绍该数据类型主要的API。这些API基本上都有 constexpr 修饰,所以能在编译时很好地处理字符串字面值,从而提高程序效率。

2.1 构造函数

constexpr string_view() noexcept;
constexpr string_view(const string_view& other) noexcept = default;
constexpr string_view(const CharT* s, size_type count);
constexpr string_view(const CharT* s);

基本上都是自解释的,唯一需要说明的是:为什么我们代码 string_view foo(string("abc")) 可以编译通过,但为什么没有对应的构造函数?

实际上这是因为 string 类重载了 stringstring_view 的转换操作符:

operator std::basic_string_view<CharT, Traits>() const noexcept;

所以, string_view foo(string("abc")) 实际执行了两步操作:

  1. string("abc") 转换为 string_view 对象 a
  2. string_view 使用对象本篇文章从 string_view 引入的背景,

2.2 自定义字面量

自定义字面量也是C++17新增的特性,提高了常量的易读。

下面的代码取值 cppreference ,能很好地说明自定义字面值和字符串语义的差异。

#include <string_view>
#include <iostream>
 
int main()
{
    using namespace std::literals;
 
    std::string_view s1 = "abc\0\0def";
    std::string_view s2 = "abc\0\0def"sv;
    std::cout << "s1: " << s1.size() << " \"" << s1 << "\"\n";
    std::cout << "s2: " << s2.size() << " \"" << s2 << "\"\n";
}

输出:

s1: 3 "abc"
s2: 8 "abc^@^@def"

以上例子能很好看清二者的语义区别, \0 对于字符串而言,有其特殊的意义,即表示字符串的结束,字符串视图根本不care,它关心实际的字符个数。

2.3 成员函数

下面列举其成员函数:忽略了函数的返回值,若函数有重载,括号内用 ... 填充。这样可以对其有个整体轮廓。

// 迭代器
begin()
end()
cbegin()
cend()
rbegin()
rend()
crbegin()
crend()
 
// 容量
size()
length()
max_size()
empty()
 
// 元素访问
operator[](size_type pos)
at(size_type pos)
front()
back()
data()
 
// 修改器
remove_prefix(size_type n)
remove_suffix(size_type n)
swap(basic_string_view& s)
 
copy(charT* s, size_type n, size_type pos = 0)
string_view substr(size_type pos = 0, size_type n = npos)
compare(...)
starts_with(...)
ends_with(...)
find(...)
rfind(...)
find_first_of(...)
find_last_of(...) 
find_first_not_of(...)
find_last_not_of(...)

从函数列表来看,几乎跟 string 的只读函数一致,使用 string_view 的方式跟 string 基本一致。有几个地方需要特别说明:

  1. string_viewsubstr 函数的时间复杂度是O(1),解决了 背景 部分的第二个问题。
  2. 修改器 中的三个函数仅会修改 string_view 的数据指向,不会修改指向的数据。

除此之外,函数名基本是自解释的。

2.4 示例

Haskell中有一个常用函数 lines ,会将字符串切割成行存储在容器里。下面我们用 C++ 来实现

string-版本

#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <sstream>

void lines(std::vector<std::string> &lines, const std::string &str) {
    auto sep{"\n"};
    size_t start{str.find_first_not_of(sep)};
    size_t end{};

    while (start != std::string::npos) {
        end = str.find_first_of(sep, start + 1);
        if (end == std::string::npos)
            end = str.length();

        lines.push_back(str.substr(start, end - start));
        start = str.find_first_not_of(sep, end + 1);
    }
}

上面我们用 const std::string & 类型接收待分割的字符串,若我们传入指向较大内存的字符指针时,会影响程序效率。

使用 std::string_view 可以避免这种情况:

string_view-版本

#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <sstream>
#include <string_view>

void lines(std::vector<std::string> &lines, std::string_view str) {
    auto sep{"\n"};
    size_t start{str.find_first_not_of(sep)};
    size_t end{};

    while (start != std::string_view::npos) {
        end = str.find_first_of(sep, start + 1);
        if (end == std::string_view::npos)
            end = str.length();

        lines.push_back(std::string{str.substr(start, end - start)});
        start = str.find_first_not_of(sep, end + 1);
    }
}

上面的例子仅仅是把 string 类型修改成了 string_view 就获得了性能上的提升。一般情况下,将程序中的 string 换成 string_view 的过程是比较直观的,这得益于两者的成员函数的相似性。但并不是所有的“翻译”过程都是这样的,比如:

void lines(std::vector<std::string> &lines, const std::string& str) {
    std::stringstream ss(str);
    std::string line;

    while (std::getline(ss, line, '\n')) {
        lines.push_back(line);
    }
}

这个版本使用 stringstream 实现 lines 函数。由于 stringstream 没有相应的构造函数接收 string_view 类型参数,所以没法采用直接替换的方式,所以翻译过程要复杂点。

三、使用陷阱

世上没有免费的午餐。不恰当的使用 string_view 也会带来一系列的问题。

  1. string_view 范围内的字符可能不包含 \0

#include <iostream>
#include <string_view>

int main() {
    std::string_view str{"abc", 1};

    std::cout << str.data() << std::endl;

    return 0;
}

本来是要打印 a ,但输出了 abc 。这是因为字符串相关的函数都有一条兼容C的约定: \0 代表字符串的结尾。上面的程序打印从开始到字符串结束的所有字符,虽然 str 包含的有效字符是 a ,但 cout\0 。好在这块内存空间有合法的字符串结尾符,如果 str 指向的是一个没有 \0 的字符数组,程序很有可能会出现内存问题,所以我们在将 string_view 类型的数据传入接收字符串的函数时要非常小心。

2.从 [const] char* 构造 string_view 对象时间复杂度 O(n)
这是因为获取字符串的长度需要从头开始遍历。如果对 [const] char* 类型仅仅是一些 O(1) 的操作,相比直接使用 [const] char* ,转为 string_view 是没有性能优势的。只不过是相比 const string&string_view 少了拷贝的损耗。实际上我们完全可以用 [const] char* 接收所有的字符串,但这个类型太底层了,不便使用。在某些情况下,我们转为 string_view 可能仅仅是想用其中的一些函数,比如 substr

3. string_view 指向的内容的生命周期可能比其本身短

string_view 并不拥有其指向内容的所有权,用Rust的术语来说,它仅仅是暂时 borrow (借用)了它。如果拥有者提前释放了,你还在使用这些内容,那会出现内存问题,这跟 悬挂指针 (dangling pointer)或悬挂引用(dangling references)很像。Rust专门有套机制在编译时分析变量的生命期,保证 borrow 的资源在使用期间不会被释放,但C++没有这样的检查,需要人工保证。下面列出一些典型的问题情况:

std::string_view sv = std::string{"hello world"};
string_view foo() {
    std::string s{"hello world"};
    return string_view{s};
}
auto id(std::string_view sv) { return sv; }

int main() {
    std::string s = "hello";
    auto sv = id(s + " world"); 
}

四、总结

string_view 解决了一些痛点,但同时也引入了指针和引用的一些老问题。C++标准并没有对这个类型做太多的约束,这引来的问题是我们可以像平常的变量一样以多种方式使用它,如,可以传参,可以作为函数返回值,可以做普遍变量,甚至我们可以放到容器里。随着使用场景的复杂,人工是很难保证指向的内容的生命周期足够长。所以,推荐的使用方式: 仅仅作为函数参数 ,因为如果该参数仅仅在函数体内使用而不传递出去,这样使用是安全的。

请关注我的公众号哦。

【现代C++】性能控的 <a href='https://www.codercto.com/tool.html'>工具</a> 箱之string_view


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

High Performance Python

High Performance Python

Andrew Lewis / O'Reilly Media, Inc. / 2010-09-15 / USD 34.99

Chapter 1. Introduction Section 1.1. The High Performance Buzz-word Chapter 2. The Theory of Computation Section 2.1. Introduction Section 2.2. Problems Section 2.3. Models of Computati......一起来看看 《High Performance Python》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器