C++17 特性:使用 std::string_view 时小心踩坑
C++17 特性:使用 std::string_view 时小心踩坑
关于 std::string_view
使用 std::string_view 的原因是为了避免无意义的 std::string 临时对象。
比如说,有某个函数,需要支持 C++-Style 的字符串,即 std::string 和 C-Style 的字符串,即 const char* 两种风格的字符串。最省事的写法就是只写一个 C++-Style 的版本,当传入 C-Style 字符串时,编译器会调用 std::string 的构造函数,自动创建一个 std::string 的临时对象。
1 | void func(const string& str); |
那么,如果这个函数会被经常调用,而你很在意这个临时对象,最好的方法是再写一个 C-Style 版本。
1 | // C++-Style version: |
写两个版本,临时对象的问题是解决了,但是这种解决方案并不那么优雅,这样做带来了更多的问题。
比如说,现在你需要维护两个版本的代码,它们的代码几乎一样。这个一般可以通过再实现一个 func_impl 函数来解决,也就是把具体实现挪到另一个函数。
又比如说,因为 func 的 C-Style 版本是模板函数,所以它的具体实现只能放到头文件里了。
那么 std::string_view 要如何“优雅地”解决问题呢?下面的代码使用 std::string_view 将 func 函数重写。
1 | // string_view version |
很多教程或者书籍都会推荐这样一个做法,一个函数有 std::string 类型的参数,如果这个参数它不会被修改,那么应该以 const-reference 的方式传递。这也就是前面 C++-Style version 的写法: 使用 const string& str
而不是 string str
。
这样做是因为以值类型传参比以引用类型传参会多一次复制,这种复制的成本可能是高昂的,需要尽量避免。
但是也有例外,如果复制的成本很小,比如 int、double 这种简单的类型,复制的成本极低,使用引用传参甚至可能拖慢速度(比如可能阻止编译器做优化)。
这里的 std::string_view 就是这种复制成本很小的对象。所以虽然我们已经习惯了使用 const string&
,但是对于 std::string_view,最好不要使用引用传参,因为 std::string_view 的本质就是一个引用,使用引用的引用并不会带来更多的好处。
容易踩坑的地方
标准库生态不佳
虽然 std::string_view 有着那么好的优点,但是想用 std::string_view 完全替代 const string&
和 const char[N]
是不会顺利的。并不是简单地把 const string&
替换成 std::string_view 就可以了。
比如说,标准库的正则表达式库 std::regex 对 std::string_view 的支持就不够好。
1 | // https://gist.github.com/WojciechMula/78f7b579abe77ebcfe38beae8d037e88 |
又比如说常用的 string to int/long long 函数,std::stoi 和 std::stoll,它们并没有提供 string_view 版本。如果你一定要将字符串转换成数字,那么只能做出修改,使用 std::from_chars 替换 std::stoi/std::stoll。
由字符串的本质引起的问题
我有一个踩坑的案例可以分享。我有一个 string_view 对象,它的内容是一个 URL,我打算使用 std::regex 从中取出 hostname 和 port,这个过程没有什么问题,而且前面也分享了如何正确地对 string_view 对象使用 std::regex。
问题在于,我得到的两个对象:std::string_view hostname;
和 std::string_view port;
它们实际储存的并不是字符串片段!
字符串的本质就是以 ‘\0’ 结尾的 char 数组(或者宽字节 wchar 数组)。string_view 在内部也就是储存了这么一条数组的指针和一个长度。
2023-01-08 Update。 我看到一种说法,觉得挺合适的:可以把 std::string_view 理解为不以 ‘\0’ 结尾的字符串,但所有的 C-Style API 都需要以 ‘\0’ 为结尾的字符串,这就是我出错的原因。
那么当你调用某些需要 C-Style 字符串的 API 时,你可以没有任何开销的将 string_view 转换成 const char*,这么做的时候你不会有任何多余的想法,因为这确实是可行的,而且没有代价。
那么如果使用 C 语言的 printf 来打印 hostname 和 port,得到的结果将会是:
1 | std::string url { "ws://localhost:8080/chat" }; |
这就导致了我调用的 C API 一直出错,我还一头雾水,一时间反应不过来为什么出错。
C++17 特性:使用 std::string_view 时小心踩坑
https://uint128.com/2022/02/16/C-17-特性-使用-std-string-view-时小心踩坑/