什么是string_view
在创建函数以将(常量)字符串作为参数时,我们有三种常见的选择:两种是我们已经知道的,另一种我们可能不知道:
// C Convention
void TakesCharStar(const char* s);
// Old Standard C++ convention
void TakesString(const std::string& s);
// string_view C++ conventions
void TakesStringView(absl::string_view s); // Abseil
void TakesStringView(std::string_view s); // C++17
当调用者已经拥有对应格式的字符串时,前两种情况效果最好,但是当需要转换(从 const char*
转换为 std::string
或从 std::string
转换为 const char*
)时,会发生什么情况?
需要将 std::string
转换为 const char*
的调用者需要使用(高效但不方便)c_str()
函数:
void AlreadyHasString(const std::string& s) {
TakesCharStar(s.c_str()); // explicit conversion
}
需要将 const char*
转换为 std::string
的调用者不需要做任何其他事情(好消息),但会调用创建一个(方便但效率低下的)临时字符串,复制该字符串的内容(坏消息):
void AlreadyHasCharStar(const char* s) {
TakesString(s); // compiler will make a copy
}
该怎么办?
接受这类字符串参数的首选选项是通过string_view
。这是 C++17 的“预采用”类型 - 现在,即使 std::string_view
可用,也应使用 absl::string_view
。
可以将 string_view
类的实例视为现有字符缓冲区的“视图”。具体来说,string_view
仅由指针和长度组成,用于标识不属于string_view
且视图无法修改的字符数据部分。因此,复制string_view
是一个浅层操作:不复制任何字符串数据。
string_view
都有来自 const char*
和 const std::string&
的隐式转换构造函数,并且由于 string_view
不复制,因此创建隐藏副本不会产生 O(n) 内存损失。在传递 const std::string&
的情况下,构造函数以 O(1) 时间运行。在传递 const char*
的情况下,构造函数会自动调用 strlen()(
或者我们可以使用双参数 string_view
构造函数)。
void AlreadyHasString(const std::string& s) {
TakesStringView(s); // no explicit conversion; convenient!
}
void AlreadyHasCharStar(const char* s) {
TakesStringView(s); // no copy; efficient!
}
由于 string_view
不拥有其数据,因此 string_view
指向的任何字符串(就像 const char*
)都必须具有已知的生命周期,并且必须比string_view
本身更持久。这意味着使用string_view
进行存储通常是值得怀疑的:我们需要一些证据来证明底层数据的寿命会超过string_view
。
如果我们的 API 只需要在单次调用期间引用字符串数据,并且不需要修改数据,则接受string_view
就足够了。
如果以后需要使用数据或修改数据,可以使用 std::string(my_string_view)
显式转换为C++字符串对象。
在现有的代码库中使用 std::string_view
作为参数类型并不总是一个最好的选择,具体原因如下:
-
与现有函数不兼容:
- 如果你正在处理的代码库中有许多函数需要
std::string
或者 NUL 终止的const char*
(C 风格字符串),那么直接将这些函数的参数类型从std::string
改成std::string_view
可能会带来性能问题。std::string_view
并不保证字符串是 NUL 终止的,也不会自动管理字符串的内存,因此在传递std::string_view
之后,如果函数还需要std::string
或者const char*
,可能需要进行额外的转换,反而降低了效率。
- 如果你正在处理的代码库中有许多函数需要
-
效率问题:
std::string_view
是一种轻量级的字符串视图,它并不拥有字符串数据,只是一个指向现有字符串数据的指针加上长度。在某些情况下,使用std::string_view
可以避免不必要的内存拷贝,从而提高性能。但是,如果后续的函数仍然需要将std::string_view
转换为std::string
或者 NUL 终止的字符串,这种性能优势就会丧失,甚至可能变得更慢,因为你可能不得不显式地创建新的std::string
对象或者进行额外的内存分配。
-
采用自下而上的方式:
- 因此,最佳的做法是从实用程序代码(utility code)开始使用
std::string_view
,并且逐渐向上推广到更高层次的代码。这意味着你可以在那些不需要将std::string_view
转换为其他类型的函数中使用它,逐步优化代码库。
- 因此,最佳的做法是从实用程序代码(utility code)开始使用
-
新项目中的一致性:
- 如果你正在开发一个新项目,可以考虑从一开始就使用
std::string_view
,以确保整个代码库的一致性。这可以帮助你更好地管理字符串数据,提高代码的可读性和性能。
- 如果你正在开发一个新项目,可以考虑从一开始就使用
另外
- 与其他字符串类型不同,我们应该按值传递
string_view
,就像传递 int 或 double 一样string_view
因为 int 是一个较小的值。 - 将
string_view
标记为const
只影响string_view
对象本身是否可以被修改,而不影响它是否可以用来修改底层字符 - 它永远不能。这与const char*
永远不能用于修改字符完全类似,即使指针本身可以被修改。 - 对于函数参数,不要在函数声明中使用
const
限定string_view
。我们可以根据自己(或我们的团队)的判断使用const
来限定函数定义中的string_view
,例如,与周围的代码保持一致。对于其他局部变量,既不鼓励也不鼓励使用const
-
string_view
不一定是 NUL 终止的。因此,编写以下内容是不安全的:printf("%s\n", sv.data()); // DON’T DO THIS
更喜欢这个:
absl::PrintF("%s\n", sv);
-
我们可以像记录字符串或
常量字符一样
记录string_view
* :LOG(INFO) << "Took '" << sv << "'";
-
关于将
const std::string&
或 NUL 终止的const char*
参数转换为std::string_view
的兼容性问题:-
现有代码的兼容性:在许多情况下,您可以将接受
const std::string&
或者 NUL 终止的const char*
的函数签名改为std::string_view
,这通常是安全且有效的做法,因为std::string_view
可以直接引用std::string
或者 C 风格的字符串(const char*
),而不需要复制数据。 -
构建中断的风险:但是,当函数指针已经被使用时,改用
std::string_view
可能会导致构建错误。原因是函数指针的类型与原来的函数签名不再匹配。例如,如果某个地方有代码保存了指向一个接受const std::string&
或const char*
的函数的指针,而你修改了该函数的签名让它接受std::string_view
,那么这个函数指针的类型就不再匹配,编译器会报告错误。这是因为std::string_view
与const std::string&
或const char*
是不同的类型,即使它们在使用上可以互相兼容。总结:主要是说明在重构现有代码时会遇到哪些问题
#include <iostream> #include <string> #include <string_view> // 原来的函数,接受 const std::string& 参数 void printString(const std::string& str) { std::cout << "String: " << str << std::endl; } // 原来的函数,接受 const char* 参数 void printCString(const char* str) { std::cout << "C-String: " << str << std::endl; } int main() { // 定义一个函数指针,指向接受 const std::string& 的函数 void (*funcPtr1)(const std::string&) = &printString; // 使用函数指针调用函数 std::string myStr = "Hello, World!"; funcPtr1(myStr); // 定义一个函数指针,指向接受 const char* 的函数 void (*funcPtr2)(const char*) = &printCString; // 使用函数指针调用函数 const char* myCStr = "Hello, C-World!"; funcPtr2(myCStr); // 现在假设我们将函数签名更改为 std::string_view // void printString(std::string_view str) { ... } // void printCString(std::string_view str) { ... } // 编译时将出现问题,因为函数指针类型与新函数签名不匹配: // funcPtr1 = &printString; // 错误!无法转换 void(*)(std::string_view) 为 void(*)(const std::string&) // funcPtr2 = &printCString; // 错误!无法转换 void(*)(std::string_view) 为 void(*)(const char*) return 0; }
-
-
std::string_view
的析构函数不会释放内存,因为它不拥有内存,只是对现有字符串的一个视图。它非常轻量,但使用时需要注意字符串的生命周期,以避免悬空引用。