什么是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 作为参数类型并不总是一个最好的选择,具体原因如下:

  1. 与现有函数不兼容

    • 如果你正在处理的代码库中有许多函数需要 std::string 或者 NUL 终止的 const char*(C 风格字符串),那么直接将这些函数的参数类型从 std::string 改成 std::string_view 可能会带来性能问题。std::string_view 并不保证字符串是 NUL 终止的,也不会自动管理字符串的内存,因此在传递 std::string_view 之后,如果函数还需要 std::string 或者 const char*,可能需要进行额外的转换,反而降低了效率。
  2. 效率问题

    • std::string_view 是一种轻量级的字符串视图,它并不拥有字符串数据,只是一个指向现有字符串数据的指针加上长度。在某些情况下,使用 std::string_view 可以避免不必要的内存拷贝,从而提高性能。但是,如果后续的函数仍然需要将 std::string_view 转换为 std::string 或者 NUL 终止的字符串,这种性能优势就会丧失,甚至可能变得更慢,因为你可能不得不显式地创建新的 std::string 对象或者进行额外的内存分配。
  3. 采用自下而上的方式

    • 因此,最佳的做法是从实用程序代码(utility code)开始使用 std::string_view,并且逐渐向上推广到更高层次的代码。这意味着你可以在那些不需要将 std::string_view 转换为其他类型的函数中使用它,逐步优化代码库。
  4. 新项目中的一致性

    • 如果你正在开发一个新项目,可以考虑从一开始就使用 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_viewconst 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 的析构函数不会释放内存,因为它不拥有内存,只是对现有字符串的一个视图。它非常轻量,但使用时需要注意字符串的生命周期,以避免悬空引用。

posted @ 2024-08-11 10:32  daligh  阅读(64)  评论(0编辑  收藏  举报