C++17 string_view的原理

Share on:
1200 Words | Read in about 6 Min | View times

Overview

字符串操作是我们日常开发过程中最常见的一环。在C++中,我们会经常结合C风格字符串(字符串字面值、字符数组、字符串指针)、std::string等方式来传递。而这些常见手段总会面临需要进行数据拷贝或者其他较为耗时的操作。C++17引入了std::string_view来解决这些问题,非常类似于Golang中的切片。本节内容就着重介绍std::string_view的原理。

本系列文章将包括以下领域:

本章其他内容请见 《现代C++》

背景

C++中有两类字符串,即C风格字符串和std::string对象两大类。

  • C风格字符串
 1#include <string.h>
 2
 3//C风格字符串初始化方式
 4char* arr = "zhxilin";
 5char arr[] = "zhxilin";
 6char arr[] = { 'z', 'h', 'x', 'l', 'i', 'n', '\0' }; //结尾必须有\0结束符
 7
 8//C风格字符串函数
 9strlen(arr);
10strcmp(arr1, arr2);
11strcat(arr1, arr2);
12strcpy(arr1, arr2);
  • C++ std::string对象
 1#include <string>
 2
 3//初始化方式
 4std::string s1;
 5std::string s2(s1);
 6std::string s3 = s1;
 7std::string s4("zhxilin");
 8std::string s4 = "zhxilin";
 9std::string s5 = std::string("zhxilin");
10std::string s6(5, 'a'); //aaaaa
11
12//对象操作
13s1.empty();
14s1.size();
15s[n];
16s.substr(3, 5);

当我们需要将字符串作为参数传递给函数时,往往会伴随字符串的拷贝。当数据占用较大内存时,减少数据的拷贝显得尤为重要。

在C++17之前,可以通过C风格字符串指针作为函数形参,也可以通过std::string字符串引用类型作为函数形参。但是这并不完美,从实践上看,存在以下问题:

  • C风格字符串的传递仍会进行拷贝

字符数组、字符串字面量和字符串指针是可以隐式转换为std::string对象的,当函数的形参是std::string,而传递的实参是C风格字符串时,编译器会做一次隐式转换,生成一个临时的std::string对象,再让形参指向这个对象。字符串字面值一般较小,性能消耗可以忽略不计;但是字符数组和字符串指针往往较大,频繁的数据拷贝就会造成较大的性能消耗,不得不重视。

  • substr()的复杂度是O(N)

std::string提供了一个返回字符串字串的函数,但是每次返回的都是一个新的对象,也需要进行构造。

那么有没有办法在原始字符串的基础上进行操作呢?答案是std::string_view

std::string_view是什么?

std::string_view顾名思义,是一个字符串的视图,类似于Golang的slice,视图并不会真正分配存储空间,而只是原始数据的一个只读窗口,可以认为它是一个内存的观察者。std::string_view的结构非常简单,只会保持原始字符串的起始指针以及字符串的长度,这个结构不会占用太多内存,开销非常小。

std::string_view定义于C++标准库头文件<string_view>中:

1template<class CharT, class Traits = std::char_traits<CharT>>
2class basic_string_view;
3
4using std::string_view = std::basic_string_view<char>;
5using std::wstring_view = std::basic_string_view<wchar_t>;
6using std::u8string_view = std::basic_string_view<char8_t>;//C++20
7using std::u16string_view = std::basic_string_view<char16_t>;
8using std::u32string_view = std::basic_string_view<char32_t>;

std::string_view实际上是一种模板类basic_string_view的一种实现。与之类似的还有wstring_viewu8string_viewu16string_viewu32string_view

构造函数

1//默认构造函数
2constexpr basic_string_view() noexcept;
3//拷贝构造函数
4constexpr basic_string_view(const string_view& other) noexcept = default;
5//直接构造,构造一个从s所指向的字符数组开始的前count个字符的视图
6constexpr basic_string_view(const CharT* s, size_type count);
7//直接构造,构造一个从s所指向的字符数组开始,到\0之前为止的视图,不包含空字符
8constexpr basic_string_view(const CharT* s);

另外std::string类重载了从stringstring_view的转换操作符:

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

因此可以通过std::string来构造一个std::string_view

1std::string_view foo(std::string("zhxilin"));

这个过程其实包含三步:第一步构造std::string的临时对象a,第二步通过转换操作符将临时对象a转换为string_view类型的临时对象b,第三步调用std::string_view的拷贝构造函数。

成员函数

迭代器

  • begin()cbegin()

  • end()cend()

  • rbegin()crbegin()

  • rend()crend()

这些成员函数与std::basic_string的相同成员函数完全兼容,可以认为是对其调用的一层封装。

元素访问

  • operator[]

  • at()

  • front()

  • back()

  • data()

返回指向底层字符串数组的指针,该指针满足范围[data(), data() + size()),且其中的值与视图的值对应。

不同于std::basic_string::data()和字符串字面量,data()可以返回指向非空终止的缓冲区的指针。

 1#include <string_view>
 2
 3using namespace std::string_view_literals;
 4
 5int main() {
 6    std::string_view sv("hello, zhxilin");
 7    std::cout << "sv = " << sv
 8        << ", size() = " << sv.size()
 9        << ", data() = " << sv.data() << std::endl;
10
11    std::string_view sv2 = sv.substr(0, 5);
12    std::cout << "sv2 = " << sv2
13        << ", size() = " << sv2.size()
14        << ", data() = " << sv2.data() << std::endl;
15
16    std::string_view sv3 = "hello\0 zhxilin"sv;//或std::string_view sv4("hello\0 zhxilin"sv)
17    std::cout << "sv3 = " << sv3
18        << ", size() = " << sv3.size()
19        << ", data() = " << sv3.data() << std::endl;
20
21    std::string_view sv4("hello\0 zhxilin");
22    std::cout << "sv4 = " << sv4
23        << ", size() = " << sv4.size()
24        << ", data() = " << sv4.data() << std::endl;
25}

运行结果为:

1sv = hello, zhxilin, size() = 14, data() = hello, zhxilin
2sv2 = hello, size() = 5, data() = hello, zhxilin
3sv3 = hello zhxilin, size() = 14, data() = hello
4sv4 = hello, size() = 5, data() = hello

从运行结果可以看出,data()会返回的是起始位置的字符指针(const char*),以data()返回值进行打印会一直输出直到遇到空字符。因此使用data()需要非常小心。

容量

  • size()length()

  • max_size()

返回可以容纳的最大长度。

1std::string_view sv;
2std::cout << sv.max_size() << std::endl; //4611686018427387899
  • empty()

修改器

  • remove_prefix()

视图的起始位置向后移动n位,收缩视图的大小:

1std::string str = "   hello";
2std::string_view v = str;
3v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
4std::cout << "String: '" << str << "', View  : '" << v << << "'" << std::endl; //String: '   hello', View  : 'hello'
  • remove_suffix()

类似remove_prefix(),视图的结束位置向前移动n位,收缩视图大小。

  • swap()

其他操作

  • copy()
1size_type copy(CharT* dest, size_type count, size_type pos = 0) const;

拷贝子串[pos, pos + rcount)dest所指向的字符序列中,其中rcount = min(count, size() - pos)

如果pos > size(),则会抛出std::out_of_range异常。

  • substr()
1constexpr basic_string_view substr(size_type pos = 0, size_type count = npos ) const;

返回子串[pos, pos + rcount)的视图,其中rcount = min(count, size() - pos),不同于std::string::substr()的时间复杂度O(n),它的时间复杂度是O(1)

如果pos > size(),则会抛出std::out_of_range异常。

  • compare()
 1constexpr int compare(basic_string_view v) const noexcept;
 2
 3constexpr int compare(size_type pos1, size_type count1, basic_string_view v) const;
 4//等价于substr(pos1, count1).compare(v)
 5
 6constexpr int compare(size_type pos1, size_type count1, basic_string_view v, size_type pos2, size_type count2) const;
 7//等价于substr(pos1, count1).compare(v.substr(pos2, count2))
 8
 9constexpr int compare(const CharT* s) const;
10//等价于compare(basic_string_view(s))
11
12constexpr int compare(size_type pos1, size_type count1, const CharT* s) const;
13//等价于substr(pos1, count1).compare(basic_string_view(s))
14
15constexpr int compare(size_type pos1, size_type count1, const CharT* s, size_type count2) const;
16//等价于substr(pos1, count1).compare(basic_string_view(s, count2))

比较两个视图是否相等。令rlen = min(size(), v.size()),该函数通过调用traits::compare(data(), v.data(), rlen)比较两个视图:

a) traits::compare(data(), v.data(), rlen) < 0时,*this小于v,返回值 < 0

b) traits::compare(data(), v.data(), rlen) == 0时,

size() < v.size(),则*this小于v,返回值 < 0

size() == v.size(),则*this等于v,返回值为0

size() > v.size(),则*this大于v,返回值 > 0

c) traits::compare(data(), v.data(), rlen) > 0时,*this大于v,返回值 > 0

 1#include <string_view>
 2
 3int main() {
 4    using std::operator""sv;
 5
 6    static_assert("abc"sv.compare("abcd"sv) < 0);
 7    static_assert("abcd"sv.compare("abc"sv) > 0);
 8    static_assert("abc"sv.compare("abc"sv) == 0);
 9    static_assert(""sv.compare(""sv) == 0);
10}
  • starts_with() (C++20)
1constexpr bool starts_with(basic_string_view sv) const noexcept;
2constexpr bool starts_with(CharT c) const noexcept;
3constexpr bool starts_with(const CharT* s) const;

判断视图是否以以给定的前缀开始。

  • ends_with() (C++20)
1constexpr bool ends_with(basic_string_view sv) const noexcept;
2constexpr bool ends_with(CharT c) const noexcept;
3constexpr bool ends_with(const CharT* s) const;

判断视图是否以给定的后缀结尾。

  • contains() (C++23)
1constexpr bool contains(basic_string_view sv) const noexcept;
2constexpr bool contains(CharT c) const noexcept;
3constexpr bool contains(const CharT* s) const;

判断视图是否包含给定的子串。

  • find()
1constexpr size_type find(basic_string_view v, size_type pos = 0) const noexcept;
2constexpr size_type find(CharT ch, size_type pos = 0) const noexcept;
3constexpr size_type find(const CharT* s, size_type pos, size_type count) const;
4constexpr size_type find(const CharT* s, size_type pos = 0) const;

返回首次出现给定子串的位置。

  • rfind()
1constexpr size_type rfind(basic_string_view v, size_type pos = npos) const noexcept;
2constexpr size_type rfind(CharT c, size_type pos = npos) const noexcept;
3constexpr size_type rfind(const CharT* s, size_type pos, size_type count) const;
4constexpr size_type rfind(const CharT* s, size_type pos = npos) const;

返回最后一次出现给定子串的位置。

  • find_first_of()
1constexpr size_type find_first_of(basic_string_view v, size_type pos = 0) const noexcept;
2constexpr size_type find_first_of(CharT c, size_type pos = 0) const noexcept;
3constexpr size_type find_first_of(const CharT* s, size_type pos, size_type count) const;
4constexpr size_type find_first_of(const CharT* s, size_type pos = 0) const;

返回首次出现给定子串中任意一个字符的位置。

  • find_last_of()
1constexpr size_type find_last_of(basic_string_view v, size_type pos = npos) const noexcept;
2constexpr size_type find_last_of(CharT c, size_type pos = npos) const noexcept;
3constexpr size_type find_last_of(const CharT* s, size_type pos, size_type count) const;
4constexpr size_type find_last_of(const CharT* s, size_type pos = npos) const;

返回最后一次出现给定子串中任意一个字符的位置。

  • find_first_not_of()
1constexpr size_type find_first_not_of(basic_string_view v, size_type pos = 0) const noexcept;
2constexpr size_type find_first_not_of(CharT c, size_type pos = 0) const noexcept;
3constexpr size_type find_first_not_of(const CharT* s, size_type pos, size_type count) const;
4constexpr size_type find_first_not_of(const CharT* s, size_type pos = 0) const;

返回首次出现不在给定子串中全部字符的位置。

  • find_last_not_of()
1constexpr size_type find_last_not_of(basic_string_view v, size_type pos = npos) const noexcept;
2constexpr size_type find_last_not_of(CharT c, size_type pos = npos) const noexcept;
3constexpr size_type find_last_not_of(const CharT* s, size_type pos, size_type count) const;
4constexpr size_type find_last_not_of(const CharT* s, size_type pos = npos) const;

返回最后一次出现不在给定子串中全部字符的位置。

std::string_view为什么性能高?

我们一直在强调std::string_view非常高效,原因如下:

  • std::string_view采用享元设计模式,通常以ptrlength的结构来实现,非常轻便。

  • std::string_view上的字符串操作具有和std::string同类操作一致的复杂度。

  • std::string_view中的字符串操作大多数是constexpr的,都可在编译器执行,省去了运行时的复杂度。

std::string_view的使用陷阱

  • data()的使用需谨慎

前面介绍这个函数的时候,我们有提到过,data()会返回的是起始位置的字符指针,若以其返回值进行输出打印,会一直输出直到遇到\0结束符。

如果通过一个没有\0的字符数组来初始化一个std::string_view对象,很有可能会出现内存问题,所以我们在将std::string_view类型的数据传入接收字符串的函数时要非常小心。

  • 要注意引用对象的生命周期

std::string_view不持有所指向内容的所有权,所以如果把std::string_view局部变量作为函数返回值,则在函数返回后,内存会被释放,将出现悬垂指针或悬垂引用。

1std::string_view foo() {
2    std::string s { "hello, zhxilin" };
3    return std::string_view { s };
4}
5
6int main() {
7    std::cout << foo() << std::endl; //可能的输出:=�;V
8    return 0;
9}

std::string_view源码解析

 1//<string_view>
 2template<typename _CharT, typename _Traits = std::char_traits<_CharT>>
 3class basic_string_view
 4{
 5public:
 6
 7    // types
 8    using traits_type = _Traits;
 9    using value_type = _CharT;
10    using pointer = value_type*;
11    using const_pointer = const value_type*;
12    using reference = value_type&;
13    using const_reference = const value_type&;
14    using const_iterator = const value_type*;
15    using iterator = const_iterator;
16    using const_reverse_iterator = std::reverse_iterator<const_iterator>;
17    using reverse_iterator = const_reverse_iterator;
18    using size_type = size_t;
19    using difference_type = ptrdiff_t;
20    static constexpr size_type npos = size_type(-1);
21
22    constexpr basic_string_view() noexcept
23        : _M_len{0}, _M_str{nullptr}
24    { }
25
26    constexpr basic_string_view(const basic_string_view&) noexcept = default;
27
28    constexpr basic_string_view(const _CharT* __str) noexcept
29        : _M_len{traits_type::length(__str)}, _M_str{__str}
30    { }
31
32    constexpr basic_string_view(const _CharT* __str, size_type __len) noexcept
33        : _M_len{__len}, _M_str{__str}
34    { }
35
36    //...
37
38private:
39
40    size_t _M_len;
41    const _CharT* _M_str;
42};

可以从整个模板类的实现上看到,整个类型内部只保存了一个原始字符串的指针和一个长度。构造函数只是对这两个成员变量进行初始化。

接下来看几个成员函数的实现:

 1//<string_view> class basic_string_view
 2constexpr const_pointer data() const noexcept
 3{
 4    return this->_M_str;
 5}
 6
 7constexpr void remove_prefix(size_type __n) noexcept
 8{
 9    __glibcxx_assert(this->_M_len >= __n);
10    this->_M_str += __n;
11    this->_M_len -= __n;
12}
13
14constexpr void remove_suffix(size_type __n) noexcept
15{
16    this->_M_len -= __n;
17}
18
19constexpr basic_string_view substr(size_type __pos = 0, size_type __n = npos) const noexcept(false)
20{
21    __pos = std::__sv_check(size(), __pos, "basic_string_view::substr");
22    const size_type __rlen = std::min(__n, _M_len - __pos);
23    return basic_string_view{_M_str + __pos, __rlen};
24}

std::string_view的实现还是非常简洁明了的,稍有经验就能很快读懂。

Prev Post: 『C++17折叠表达式』
Next Post: 『C++17 constexpr的改进』