本篇文章从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++】性能控的工具箱之string_view相关推荐

  1. vs2022安装dev控件工具箱不显示

    我的vs版本是2022的,然后第一次安装dev控件我下的是20.2版本的. 安装完了之后工具箱中一直没有显示dev相关控件. 然后我尝试过修复,还是没有达到目的.试过网上的办法"Toolbo ...

  2. string_View理解与用法(一)

    什么是string_view 当你创建一个将(常量)字符串作为参数的函数时,你有四个选择,你可能知道两个,但不知道另外两个: void TakesCharStar(const char* s); // ...

  3. 【现代C++】新的字符串格式化方法

    (点击上方公众号,可快速关注) 本篇文章主要介绍现代C++字符串格式化的方法.在此之前,回顾了一些老的字符串格式化的方法,并分析各自的优劣.在最后给出了一种提供给老编译器的折中方案,因为新的格式化方法 ...

  4. 添加Aforge控件,报错“在.....中没有可放置在工具箱上的控件”

    添加Aforge控件,报错"在-中没有可放置在工具箱上的控件" 1.先添加AForge系列引用 工具–>NuGet包管理器–>管理解决方案的NuGet程序包 之后,打开 ...

  5. MFC工具箱各种控件的具体意义

    忘记在哪看到的了, 控件工具箱:  2 图形控件(picture):常用于显示位图(Bitmap)和图标(Icon) 3 静态文本(Static Text):用来在指定的位置显示特定的字符串,一般用来 ...

  6. MFC工具箱控件的一些用法

    转自https://blog.csdn.net/qq_34174814/article/details/51419967 控件工具箱: 2 图形控件(picture):常用于显示位图(Bitmap)和 ...

  7. Web 窗体控件简介

    概述  Active Server Pages+ (ASP+) 框架,也称为 Web 窗体,可用来创建可编程的网页,以便将它们作为总体 Web 应用程序的组成部分.Web 窗体在以下方面简化了 Web ...

  8. java权威指南电子书下载,Java性能权威指南pdf

    市面上介绍Java的书有很多,但专注于Java性能的并不多,能游刃有余地展示Java性能优化难点的更是凤毛麟角,本书即是其中之一.通过使用JVM和Java平台,以及Java语言和应用程序接口,本书详尽 ...

  9. ATLAS入门篇之CascadingDropDown控件编程

    一.引言 本文将通过具体的步骤解释如何借助于数据库(Microsoft SQL Server 2005 Express Edition)数据创建一个ASP.NET AJAX 1.0 Cascading ...

最新文章

  1. 【转】使用Chrome Frame,彻底解决浏览器兼容问题
  2. 2、 db、tables_priv、columns_priv和procs_priv权限表
  3. 开灯变形问题(枚举法)
  4. 58端口使用技巧跟推送_Kindle使用技巧:定时推送
  5. redhat enterprise linux 5 上安装openoffice3.0 1
  6. div里面放ul,使ul横向和纵向滚动
  7. 腾讯:人们回归工作导致四季度游戏收入减缓
  8. Spark Structured : KuduException$OriginalException: Originalasynchronous stack trace
  9. window.location.href 跳转失败
  10. react中将html字符串渲染到页面
  11. mysql事务隔离级别回顾
  12. E1--千兆以太网接口测试应用2022-09-07
  13. ROS——Gazebo仿真——全向轮小车——运动学模型分析
  14. Andorid通话自动录音
  15. android系统息屏设置_安卓实现熄屏功能。
  16. 网页注册页面html代码,网页代码
  17. 计算机附录的相关文件,计算机化系统附录与计算机文件编制验证实际操作2.pptx...
  18. php微信网页登录,PHP 微信网页登录
  19. 内存泄漏和溢出的区别
  20. 《性能优化》并发与并行

热门文章

  1. 牛客网暑期ACM多校训练营(第六场) - (A,C,J)
  2. 基于java GUI实现的一个日历记事本小项目
  3. 医疗电气设备安规术语理解
  4. 流量主系列|独立版在线答题微信小程序源码
  5. Extjs3.2 Form表单
  6. 关于bgcolor和background-color的区别
  7. 【图像分割】扩散张量成像(DTI)脑白质纤维束成像【含Matlab源码 2174期】
  8. 牛奶包装袋上的秘密:不知道真的还是假的
  9. 《初等数论》:高斯函数、n的阶乘的标准分解式
  10. 【SylixOS之更新LITE版Base方式】