每周技巧 #1:`string_view`

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #1: string_view

原文最初作为 TotW #1 发布于 2012 年 4 月 20 日。

作者:Michael Chastain

更新于 2020 年 8 月 18 日。

快捷链接:abseil.io/tips/1

什么是 string_view,为什么你应该关心它?

当你创建一个函数,让它接收一个(常量)字符串参数时,通常有三种选择:其中两种你已经熟悉,另一种你可能还没注意到:

1
2
3
4
5
6
7
8
9
// C 语言约定
void TakesCharStar(const char* s);

// 旧式标准 C++ 约定
void TakesString(const std::string& s);

// string_view 的 C++ 约定
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() 函数:

1
2
3
void AlreadyHasString(const std::string& s) {
  TakesCharStar(s.c_str());               // 显式转换
}

需要把 const char* 转成 std::string 的调用方不需要额外写任何东西,这是好消息;但这会触发一个临时字符串的创建,并复制字符串内容,这是方便但低效的坏消息:

1
2
3
void AlreadyHasCharStar(const char* s) {
  TakesString(s); // 编译器会创建一份拷贝
}

应该怎么做?

Google 更推荐用 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 构造函数。

1
2
3
4
5
6
7
void AlreadyHasString(const std::string& s) {
  TakesStringView(s); // 不需要显式转换,很方便!
}

void AlreadyHasCharStar(const char* s) {
  TakesStringView(s); // 不会复制,很高效!
}

因为 string_view 不拥有数据,所以它指向的任何字符串,就像 const char* 指向的字符串一样,都必须有已知的生命周期,并且必须比 string_view 本身活得更久。

这意味着,把 string_view 用作存储字段时常常值得怀疑:你需要证明底层数据一定会比这个 string_view 活得更久。例如,如果下面这个结构体可能在接收它的函数调用结束后继续保存,那么它很可能就不合理:

1
2
3
4
struct TestScore {
  absl::string_view username;  // 这里很可能应该用 `std::string`
  double score;
};

如果你的 API 只需要在单次调用期间引用字符串数据,并且不需要修改这些数据,那么接收 string_view 就足够了。

如果你需要稍后继续使用这些数据,或者需要修改数据,可以用 std::string(my_string_view) 显式转换成 C++ 字符串对象。另一种做法见 技巧 #117:按值传递 std::string,并在适用时让调用方使用 std::move

string_view 加入已有代码库并不总是正确答案:如果这些参数随后又会传给要求 std::string 或以 NUL 结尾的 const char* 的函数,那么把参数改成按 string_view 传递可能反而低效。最好从工具代码开始逐层向上采用 string_view,或者在新项目里从一开始就保持完全一致。

几点补充说明

  • 与其他字符串类型不同,应该像传递 intdouble 一样按值传递 string_view,因为 string_view 是一个很小的值。

  • string_view 标记为 const,只会影响这个 string_view 对象本身能否被修改,并不会影响它能否用来修改底层字符。它永远不能修改底层字符。这和 const char* 完全类似:即使指针本身可以被修改,也不能通过它修改字符。

  • 对函数参数来说,不要在函数声明中用 const 修饰 string_view(见 技巧 #109)。在函数定义中,可以由你或你的团队自行决定是否用 const 修饰 string_view,例如为了和周围代码保持一致(技巧 #109)。对于其他局部变量,使用 const 既不特别鼓励,也不特别反对(见 Google C++ 风格指南)。

  • string_view 不一定以 NUL 结尾。因此,下面这种写法不安全:

    1
    
    printf("%s\n", sv.data()); // 不要这样做
    

    更推荐这样写(见 技巧 #124):

    1
    
    absl::PrintF("%s\n", sv);
    
  • 你可以像记录 std::stringconst char* 一样记录 string_view

    1
    
    LOG(INFO) << "Took '" << sv << "'";
    
  • 在多数情况下,可以安全地把一个接收 const std::string& 或以 NUL 结尾的 const char* 的已有函数改成接收 string_view。我们在执行这种改动时遇到的唯一风险是:如果有人取了这个函数的地址,构建会失败,因为改动后的函数指针类型不同。

  • string_viewconstexpr 构造函数和平凡析构函数;在静态变量和全局变量(见 风格指南)或常量(见 技巧 #140)中使用它时,需要记住这一点。

  • string_view 本质上是一种引用,因此未必适合作为成员变量(另见 技巧 #180)。

C++ 每周技巧

上一节

每周技巧 #3:字符串拼接:`operator+` 与 `StrCat()`

下一节