加载中...
string_view 适合用做函数的返回值类型吗?
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
第26节:C++模板可变参数如何一次性解包?
第27节:Linux下的c++开发,平时是怎么调试代码的呢?
课文封面

满足一个条件即可:返回的内容本体不会改变(包括一直活着)

0. 问题

C++17引入了 std::string_view 类型,请问适合用它来作为某个函数或方法的返回值类型吗?比如这样一个函数原型:std::string_view getName()

1. 基本分析

std::string_view 是一个原有的字符串(包括std::string 或 C风格的裸字符串)的“观察者”。
并不真实拥有字符串的内容,因此也无法确保所观察的字符串内容的生命周期——有可能有观察一半时,字符串就被回收了……

有关 std::string_view 的基础知识与及实际使用的一个非常棒的例子,请看本站课堂:《Hello World 函数版》

由于无法自主掌控数据内容以及数据生命周期,所以通常函数,特别是自由函数比较少返回一个 string_view 对象;成员函数(俗称类的方法)相对会多一些,因为成员函数可以返回当前对象拥有的某个字符串的内容,而对象对其所拥有的内容,比较容易确保其生命周期。

下面分别给出自由函数与成员函数返回 std::string_view 的例子。

示例1:自由函数

自由函数要返回 string_view,这通常意味着,它会在函数体内拥有一个 static 数据——如果不这样,就只能返回一个位于函数外部的全局数据了。正常情况下,两害取其轻,在增加一个全局数据和增加一个函数内静态数据之间,我们会选择前者。

enum class Color {Red, Green, Blue}; std::string_view GetColorName(Color color) { // static 用以保障数据的生命周期 static char const* names[]= {"仇人红", "原谅绿", "生活蓝"}; auto i = static_cast<std::size_t>(color); return (i >= 0 && i <= 3)? names[i] : ""; }

这个例子返回 string_view 是挺合适的——

  • 首先,函数内的静态数据可以保障返回的数据(来自 names )一直存活,确保套壳其上的观察者 string_view 不会在使用期间失效;
  • 其次,以 string_view 作为返回对象的类型,可明确告知调用者:你只能“察看”或复制走数据,而不能(直接)修改它们—— string_view 就没有修改数据的功能。

作为对比,在 C++17 之前该函数的返回类型通常是:char const* const ,或至少为 char const* 。 函数使用者通过 char const * 明白不能修改得到的内容,通过 * const 得到信息:不用也不能 delete 得到的指针。

示例2:成员函数

成员函数返回一个 string_view,则其观察的内容,通常就是来自该类的其它成员。

class Book { public: Book(std::string name, double price) : name(std::move(name)), price (price) {} std::string_view GetName() const { return name; } double GetPrice() const { return price; } private: std::string name; // 书名 double price; // 价格 }; int main() { Book b("飘", 12.00); std::cout << "《" << b.GetName() << "》" << std::endl; }

注意,如果希望 Book::GetName() 自动地临时在书名前后加上一对书名号,返回 string_view 就马上变得不合适了:

// 有 bug : 返回的临时 string 生命周期无保障 std::string_view GetName() const { return "《" + name + "》"; }

此情况下,应乖乖返回一个 std::string 实体。

2. 基于STL实例分析

举个标准库的“求子串”功能在string 和 string_view 身上的实现,作对比——

求子串:

// string::substr() string substr( size_type pos = 0, size_type count = npos); // string_view::substr(); string_view substr( size_type pos = 0, size_type count = npos);

如果我们手上有一个生命周期尚在的 std::string_view,并且需要求子串,那自然就使用 string_view::substr();但是,如果我们手上有一个 std::string 对象,那我们就无脑使用 std::string::substr()吗?还是说,某些情况下应该使用 std::string_view::substr() 呢?

  1. 如果要在一个字符串(以下称为母串)身上执行很多、很多次的子串查找
  2. 并且可以确保这个母串在此操作过程中,始终都没人碰它(包括内容不变,以及对象不死),
  3. 那我们就可以考虑使用 std::string_view 的版本;
  4. 又如果,(在此期间)我们还要把所有查出来的子串都存储起来……
  5. 那就更值得使用 string_view 了。

原因很简单:substr(…),返回一个 std::string 结果,就得实打实地为这个结果分配存储字符内容的内存。比如,假设母串 1000 个字节,而 某次查到的子串不幸长 999个字节,那么 现在母串和子串加起来,就至少占用 1999 个字节。

然而,子串的 999 个字符,齐齐整整地母串里排着,并且又不改它们,何必要为它们分配独立内存呢?

std::string_view 当然也需要占用内存,但它占用的字节数是稳定的,不随子串的长短而变。综合考虑下来,通常是 std::string_view 会占用更少内存。

最后再给个生造的对比例子,用于测试性能,以验证上述说法。

#include <iostream> #include <string> #include <string_view> #include <utility> #include <vector> #include <chrono> template<typename T> void test (std::string_view name, T const& s, int count) { std::cout << "[" << name << "]:\n"; std::vector<T> result; namespace cho = std::chrono; auto start = cho::system_clock::now(); for (auto i=0; i<count; i++) { auto s1 = s.substr(10); auto s2 = s.substr(5, 3); auto s3 = s.substr(s.size() - 3, 50); result.push_back(std::move(s1)); result.push_back(std::move(s2)); result.push_back(std::move(s3)); } auto ms = cho::duration_cast<cho::milliseconds>(cho::system_clock::now() - start); std::cout << result[0] << "," << result[count / 3] << "," << result[count / 2] << "," << result[count * 2 / 3] << "," << result[count / 2] << "," << result[count] << "," << result[count * 2] << "," << result[count * 3 - 1] << "\n"; std::cout << ms.count() << std::endl; } int main() { int const count = 10'000'000; std::string data = "0123456789abcdefghij"; { test("string_data", data, count); } { test("string_view", std::string_view{data}, count); } return 0; }

在线运行以上代码

某次运行结果 :

[string_data]:
abcdefghij,abcdefghij,hij,abcdefghij,hij,567,hij,hij
5225
[string_view]:
abcdefghij,abcdefghij,hij,abcdefghij,hij,567,hij,hij
3786

在所给的测试输入下,肉眼可见,string_view 版本快。

试过对调两次 test() 的前后次序,结果没有实质变化,string_view版本总是比较快。

但性能不是这段测试代码值得看的重点——关于它,我们几乎已经预料到。重点是: main() 函数里的那个名为 data 的 std::string ,它是不是那么自然而然地,活到了两次 test() 结束之后?而,两次 test() 调用也是那么坚定坚决地做到绝不对改 data 内容?或许,这就是成年人与成年人之间的信用与信任吧?