C++17 详解 20 — std::string_view

本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。

翻译太特么累人了……剩余部分还是只做摘要翻译吧。

9. std::string_view

头文件:

1
#include <string_view>

string_view 是 string 的“视图”。它不拥有 string,也不会复制其内容,但你却可以通过 string_view 进行一些 string 相关的操作,从而避免不必要的复制。

比如:

1
2
3
4
5
6
7
8
9
10
// string function:
std::string StartFromWordStr(const std::string& strArg, const std::string& word)
{
return strArg.substr(strArg.find(word)); // substr creates a new string
}

// call:
std::string str {"Hello Amazing Programming Environment" };
auto subStr = StartFromWordStr(str, "Programming Environment");
std::cout << subStr << '\n';

上边代码会执行至少三次字符串复制:

  • str 定义时
  • 函数构造第二个参数时:需要从 const char* 转换成 string 对象
  • subStr 初始化

这种 substr 操作就很适合用 string_view 进行优化:

1
2
3
4
5
6
7
8
9
std::string_view StartFromWord(std::string_view str, std::string_view word)
{
return str.substr(str.find(word)); // substr creates only a new view
}

// call:
std::string str {"Hello Amazing Programming Environment"};
auto subView = StartFromWord(str, "Programming Environment");
std::cout << subView << '\n';

上边代码只进行一次字符串的复制—— str 定义。string_view 通常只持有指向字符串起始位置的指针和长度,所以它是很轻量的。

译注:GCC 下 sizeof(string_view) = 16。所以再轻量,作为函数参数传递时该用引用还是得用!

std::basic_string_view 类型

同 string 一样,string_view 也是一系列模板类的简称:

1
2
3
4
std::string_view        std::basic_string_view<char>
std::wstring_view std::basic_string_view<wchar_t>
std::u16string_view std::basic_string_view<char16_t>
std::u32string_view std::basic_string_view<char32_t>

创建

  1. 从一个带终止符(\0)的 const char* 字符串创建
1
2
3
4
const char* cstr = "Hello World";

std::string_view sv1 { cstr };
std::cout << sv1 << ", len: " << sv1.size() << '\n'; // 11
  1. 从 const char* 和一个长度创建
1
2
3
4
const char* cstr = "Hello World";

std::string_view sv2 { cstr, 5 };
std::cout << sv2 << ", len: " << sv2.size() << '\n'; // 5
  1. 从 std::string 创建
1
2
3
std::string str = "Hello String";
std::string_view sv3 = str;
std::cout << sv3 << ", len: " << sv3.size() << '\n'; // 11
  1. 通过 “”sv 字面量创建
1
2
3
4
using namespace std::literals;
std::string_view sv4 = "Hello\0 Super World"sv;
std::cout << sv4 << ", len: " << sv4.size() << '\n';
std::cout << sv4.data() << " - till zero\n";

输出:

1
2
Hello Super World, len: 18
Hello - till zero

data() 返回指向首字符的指针,所以 cout 输出时遇到终止符时即停止打印。

其它操作

一个大概总结:

既然 string_view 是对 string 的虚代理,那合理推测是 string_view 应提供同 string 相同的操作接口;又因 string_view 不持有字符串,只做视图用,那 string_view 应剔除修改相关(即非 const)接口。

对 string_view 自身属性的访问:

  1. remove_prefix(size_t n)

将 string_view 持有的指针向前移动 n 个位置(+n),string_view::size() 随之减小 n。n > string_view::size() 行为未定义。

  1. remove_suffix(size_t n)

将 string_view::size() 减小 n。n > string_view::size() 行为未定义。

  1. swap

交换两个 string_view 对象内容,即指针和尺寸。

风险

  1. data()
1
2
3
4
5
std::string s = "Hello World";
std::cout << s.size() << '\n';
std::string_view sv = s;
auto sv2 = sv.substr(0, 5);
std::cout << sv2.data() << '\n'; /// ooops?

如之前提到的,data() 返回的是起始位置的字符指针(const char*),以 data() 返回值进行打印输出很可能得不到你想要的结果。

进行基于字符串的转换时也有此问题:

1
2
3
4
std::string number = "123.456";
std::string_view svNum { number.data(), 3 };
auto f = atof(svNum.data()); // 注意这句
std::cout << f << '\n';

你很可能期望 f 值等于 123,但实际 f = 123.456

  1. 引用对象生命周期

还是最开始时候的函数:

1
2
3
4
std::string_view StartFromWord(std::string_view str, std::string_view word)
{
return str.substr(str.find(word)); // substr creates only a new view
}

但如果以临时对象调用会怎么样?

1
2
3
auto str = "My Super"s;
auto sv = StartFromWord(str + " String", "Super");
// 开始使用 sv

对 sv 的操作将导致运行时错误。

译注:

其实 string_view 跟智能指针的设计思路是一样的,都是享元设计模式的具体体现。

因为 string_view 处理的是字符串相关,在通常的引用对象生命周期问题外引入了字符串领域的空字符问题。这其实也不是一个新鲜问题,如果你有用 string 作为字节缓冲区(而非字符串)的经历,你一定也对 string::data() 中的坑不陌生。

性能 & 内存考量

  1. string_view 通常以 [ptr, length] 结构实现,很轻量;

  2. string_view 上的字符串操作同 string 中的同类操作具有一致的复杂度;

  3. string_view 中的字符串操作函数绝大多数都是 constexpr 的:

1
2
3
4
5
6
7
8
9
10
11
#include <string_view>
int main()
{
using namespace std::literals;

constexpr auto strv = "Hello Programming World"sv;
constexpr auto strvCut = strv.substr("Hello "sv.size());

static_assert(strvCut == "Programming World"sv);
return strvCut.size();
}

如上,strv、strvCut 都是在编译期构造的。

评论