什么是string_view
当你创建一个将(常量)字符串作为参数的函数时,你有四个选择,你可能知道两个,但不知道另外两个:
void TakesCharStar(const char* s); // C convention
void TakesString(const string& s); // Old Standard C++ convention
void TakesStringView(absl::string_view s); // Abseil C++ convention
void TakesStringView(std::string_view s); // C++17 C++ convention
当调用者已经有已提供的格式的字符串时,前两者方法最有效,但是当需要进行转换(如从const char *到string 或 string到char *)时发生什么呢?
调用者需要将字符串转换为const char *时,需要用(高效但不方便)c_str()函数:
void AlreadyHasString(const string& s) {
TakesCharStar(s.c_str()); // explicit conversion
}
调用者需要将const char *转换为字符串时,不需要做任何其他操作(这是好消息);但是将创建临时字符串(方便但效率低),并复制该字符串的内容(这是坏消息)。
void AlreadyHasCharStar(const char* s) {
TakesString(s); // compiler will make a copy
}
String有什么缺点
本节内容主要摘自博客【现代C++】性能控的工具箱之string_view。
在数据传递中减少拷贝是提高性能的最常用办法。在C中指针是完成这一目的的标准数据结构,而在C++中引入了安全性更高的引用类型。所以在C++中若传递的数据仅仅可读,const string&成了C++天然的方式。但这并非完美,从实践上来看,它至少有以下几方面问题:
- 字符串字面值、字符数组、字符串指针的传递依然要数据拷贝
这三类低级数据类型与string类型不同,传入时编译器要做隐式转换,即需要拷贝这些数据生成string临时对象。const string&指向的实际上是这个临时对象。通常字符串字面值较小,性能损失可以忽略不计;但字符串指针和字符数组某些情况下可能会比较大(比如读取文件的内容),此时会引起频繁的内存分配和数据拷贝,影响程序性能。
- substr O(n)复杂度
substr是个常用的函数,好在std::string提供了这个函数,美中不足的时每次都要返回一个新生成的子串,很容易引起性能热点。实际上我们本意不是要改变原字符串,为什么不在原字符串基础上返回呢?
怎么办
在C++17中引入了string_view
,能很好的解决以上两个问题。
std::string_view是C++ 17标准中新加入的类,正如其名,它提供一个字符串的视图,即可以通过这个类以各种方法“观测”字符串,但不允许修改字符串。由于它只读的特性,它并不真正持有这个字符串的拷贝,而是与相对应的字符串共享这一空间。即——构造时不发生字符串的复制。同时,你也可以自由的移动这个视图,移动视图并不会移动原定的字符串。
- 通过调用 string_view 构造器可将字符串转换为 string_view 对象。string 可隐式转换为 string_view。
- string_view 是只读的轻量对象,它对所指向的字符串没有所有权。
- string_view通常用于函数参数类型,可用来取代 const char* 和 const string&。string_view 代替 const string&,可以避免不必要的内存分配。
- string_view的成员函数即对外接口与 string 相类似,但只包含读取字符串内容的部分。
string_view::substr()的返回值类型是string_view,不产生新的字符串,不会进行内存分配。string::substr()的返回值类型是string,产生新的字符串,会进行内存分配。 - string_view字面量的后缀是 sv。(string字面量的后缀是 s)
#include <string_view>
#include <iostream>
int main()
{
using namespace std::literals;
std::string_view s1 = "abc00def";
std::string_view s2 = "abc00def"sv;
std::cout << "s1: " << s1.size() << " "" << s1 << ""n";
std::cout << "s2: " << s2.size() << " "" << s2 << ""n";
}
输出:
s1: 3 "abc"
s2: 8 "abc^@^@def"
以上例子能很好看清二者的语义区别,0
对于字符串而言,有其特殊的意义,即表示字符串的结束,字符串视图根本不care,它关心实际的字符个数。
Google首选通过stringview接受这样的字符串参数。这是C++17的“pre-adopted”类型,在C++17的构建中,您应该使用std::string_view,在任何不依赖C++17的代码中,您应该使用absl::string_view(Abseil是Google开源的C++库)。
string_view类的实例可以看作是现有字符串缓冲区的“视图”。具体来说,string_view仅由一个指针和一个长度组成,用于标记不是string _view拥有且不能被该视图修改的字符串数据部分。所以,复制string_view是一项浅层的操作:不复制任何字符串数据。
string_view有来自const char * 和 const string&的隐式转换构造函数,并且由于string_view不拷贝,因此进行浅拷贝不产生O(n)内存损失。在传递cosnt string&的情况下,构造函数在O(1)时间进行。在传递const char*的情况下,构造函数会自动调用strlen()(或者你可以使用具有两个参数的string_view构造函数)。
void AlreadyHasString(const string& s) {
TakesStringView(s); // no explicit conversion; convenient!
}
void AlreadyHasCharStar(const char* s) {
TakesStringView(s); // no copy; efficient!
}
因为string_view不拥有数据,所以string_view所指的任何字符串必须具有已知的生命周期,并且必须比string_view本身生命周期更长。这意味着使用string_view进行存储通常是有问题的:你需要一些证据证明基础数据的生命周期将超过string_view。
如果你的API仅需在一次调用中引用字符串数据,而无需修改数据,则接受string_view就足够了。如果以后需要引用数据或需要修改数据,则可以使用string(my_string_view)显式转换为C ++字符串对象。
将string_view添加到现有代码库中并非总是正确的答案:更改参数以通过string_view传递可能效率不高,如果这些参数随后传递给需要字符串或以NUL终止的const char *的函数。最好从实用程序代码开始向上使用string_view,或者在启动新项目时保持完全一致。
其他事项
- 与其他字符串类型不同,你应该按值传递string_view,就像int或double一样,因为string_view是一个很小的值。
- string_view不一定是NUL终止的。因此,编写以下内容并不安全:
printf("%sn", sv.data()); // DON’T DO THIS
但是,下面是好的代码:
printf("%.*sn", static_cast<int>(sv.size()), sv.data());
- 你可以输出string_view,就像输出字符串或const char*一样:
std::cout << "Took '" << s << "'";
- 大多数情况下,你可以将接受const string&或NUL终止的const char*的现有例程安全的转换为string_view。在执行此操作时遇到的唯一危险是,如果已获取函数的地址,则将导致编译中断,因为生成的函数指针类型将有所不同。