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() 呢?
- 如果要在一个字符串(以下称为母串)身上执行很多、很多次的子串查找
- 并且可以确保这个母串在此操作过程中,始终都没人碰它(包括内容不变,以及对象不死),
- 那我们就可以考虑使用 std::string_view 的版本;
- 又如果,(在此期间)我们还要把所有查出来的子串都存储起来……
- 那就更值得使用 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 内容?或许,这就是成年人与成年人之间的信用与信任吧?