- 序言
- 何谓StringPiece?
- StringPiece的常见使用场景
- 源码剖析StringPiece
- BasicStringPiece模板
- 构造函数
- 容量相关函数
- 数据修改函数
- 修改其他字符串的函数
- 数据访问函数
- 比较函数
- 查找函数
- 截取子串
- 返回string对象
- 从StringPiece到string_view
- 备胎转正
- API的差异
- 如果你没有C++14/17
序言
在brpc源码的src目录下,有一级子目录名为butil。代码中的util目录一般就是存放常用的工具类或函数的地方。今天我们来聊一下butil/strings/string_piece.h(cpp) 中的StringPiece类。
其实brpc项目中的StringPiece并非brpc原创,而是从Google的Chromium项目中拿过来的。
可以看下该文件的开头的注释:
// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Copied from strings/stringpiece.cc with modifications
因为Chromium项目是以BSD开源协议发布的,所以brpc拿过来用,其实并没有问题。只需要保留原项目的BSD协议声明即可。
其实StringPiece并不新鲜,在很多C++项目中都能见到类似的字符串工具类的身影。
项目 (类名) | 源码在线阅读地址 |
chromium (StringPiece) | https://github.com/chromium/chromium/blob/master/base/strings/string_piece.h |
llvm (StringRef) | https://github.com/llvm-mirror/llvm/blob/master/include/llvm/ADT/StringRef.h |
boost (string_ref) | https://github.com/boostorg/utility/blob/master/include/boost/utility/string_ref.hpp |
folly (StringPiece) | https://github.com/facebook/folly/blob/main/folly/Range.h |
pcre (StringPiece) | https://opensource.apple.com/source/pcre/pcre-4.1/pcre/pcre_stringpiece.h.in.auto.html |
leveldb (Slice) | https://github.com/google/leveldb/blob/master/include/leveldb/slice.h |
abseil (string_view) | https://github.com/abseil/abseil-cpp/blob/master/absl/strings/string_view.h |
boost (string_view) | https://github.com/boostorg/utility/blob/master/include/boost/utility/string_view.hpp |
其中boost的string_ref实现方式是参考的llvm的StringRef以及谷歌发布的这个提案:string_ref: a non-owning reference to a string
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3442.html
brpc采用了Chromium的实现,muduo采用了pcre的实现。这两个几乎是照搬的源码(brpc仅微调),故不列到上面的表格中了。
何谓StringPiece?
StringPiece和普通std::string的最大不同之处是,它并不持有数据!俗称是StringPiece没有数据的所有权。它存储的是外部字符串的数据指针,而自己并没有开辟空间在存储这份数据(字符串)。因此StringPiece中数据的生命周期并不和StringPiece等价,而是依旧和传入的数据指针的来源一致。
比如下面这段代码就是高危操作:
StringPiece foo() { std::string str = "xxx"; ... return StringPiece (str); // 函数返回时,StringPiece持有的数据(属于str)已经析构 } void bar() { StringPiece sp = foo(); // 后面再通过sp访问其中的字符串时危险的 cout<<sp.data() << endl; // 危险! }
StringPiece的常见使用场景
看完上面例子,你会不会感觉StringPiece太危险了,那它存在有必要吗?答案是肯定的,因为有时候尽管我们需要字符串中的一段数据,但并不需要潜在的字符串拷贝。最常见的例子就是字符串拆分的时候。、字符串切分是一个高频操作,在NLP领域,这个操作被称为tokenize,切分出来的单词称为token。
但C++没有像其他语言一样提供官方的std::string的split()函数,为此各种实现五花八门。比如这种:
void split(std::vector<std::string> &vec, const std::string& str, const std::string& sep) { size_t start = str.find_first_not_of(sep); size_t end; while (start != std::string::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string::npos) { //vec.push_back(str.substr(start)); vec.emplace_back(str, start); break; } else { //vec.push_back(str.substr(start, end - start)); vec.emplace_back(str, start, end - start); } start = str.find_first_not_of(sep, end + 1); } }
这里其实就是会产生临时字符串拷贝的开销,每个被切割出来的token字符串都会拷贝到vector容器中。但有时候在我们接下来的使用观察中,我们对于切分好的token其实并不会去修改它,都是只读操作。
换句话说我们只是观测它而已,那么我们完全没必要这么多token的拷贝!这时候StringPiece就派上用场了。你可以将传入的string,赋值给一个StringPiece对象,然后让StringPiece去参与split的过程,最后存储到vector的容器中。
这样整个过程完全没有字符串拷贝的开销。当然如果你split完的字符串,后续你需要修改它,那么用StringPiece依旧是危险的。
源码剖析StringPiece
下面针对StringPiece源码的解读是基于brpc中代码,也就是Chromium的实现来展开。这一节很长,你可能感觉枯燥,你可以根据目录,选择性地查看相关内容。
BasicStringPiece模板
StringPiece其实是BasicStringPiece类模板用std::string实例化后的类型:
typedef BasicStringPiece<std::string> StringPiece;
来看BasicStringPiece模板的定义:
template <typename STRING_TYPE> class BasicStringPiece { public: // Standard STL container boilerplate. typedef size_t size_type; typedef typename STRING_TYPE::value_type value_type; typedef const value_type* pointer; typedef const value_type& reference; typedef const value_type& const_reference; typedef ptrdiff_t difference_type; typedef const value_type* const_iterator; typedef std::reverse_iterator<const_iterator> const_reverse_iterator; static const size_type npos; ...
先记住两个类型:
- STRING_TYPE是模板参数类型,对StringPiece而言,STRING_TYPE也就是std::string
- value_type是STRING_TYPE::value_type的别名,对StringPiece而言,也就是std::string::value_type ,即 char类型。
再看看唯二的成员变量:
protected: const value_type* ptr_; size_type length_;
一个指针ptr_指向字符串数据,还有一个length_标记该字符串的长度。一个指针类型,一个size_type类型,所以StringPiece基本就是两个long的大小,轻量的很。
构造函数
再来看看它的构造函数:
BasicStringPiece() : ptr_(NULL), length_(0) {} BasicStringPiece(const value_type* str) : ptr_(str), length_((str == NULL) ? 0 : STRING_TYPE::traits_type::length(str)) {} BasicStringPiece(const STRING_TYPE& str) : ptr_(str.data()), length_(str.size()) {} BasicStringPiece(const value_type* offset, size_type len) : ptr_(offset), length_(len) {} BasicStringPiece(const BasicStringPiece& str, size_type pos, size_type len = npos) : ptr_(str.data() + pos), length_(std::min(len, str.length() - pos)) {} BasicStringPiece(const typename STRING_TYPE::const_iterator& begin, const typename STRING_TYPE::const_iterator& end) : ptr_((end > begin) ? &(*begin) : NULL), length_((end > begin) ? (size_type)(end - begin) : 0) {}
可以看出StringPiece支持传入C风格字符串(const char*),也支持传入std::string。甚至你也可以指定传入的std::string的起始位置以及长度。也就是说StringPiece不必持有全部的原始std::string的字符串,而是可以只取其一段。当然普通的std::string的构造函数也支持传入另外一个std::string并指定其起始位置和长度,但是std::string的做法是将原字符串的这一小段字符串拷贝到自己的堆存储中来,后面就和原字符串没有瓜葛了。但StringPiece却不会做这个拷贝操作,所以它依旧和原始字符串藕断丝连!
这里指的一提的是当只传入const char*,不指定长度的时候。StringPiece会调用string::traits_type::length()函数来计算长度。
http://www.cplusplus.com/reference/string/char_traits/length
Get length of null-terminated stringReturns the length of thenull-terminatedcharacter sequences.
返回的是'\0'结尾的字符串的长度。如果你传入的const char*类型的字符串中间有'\0'会被截断。
容量相关函数
const value_type* data() const { return ptr_; } size_type size() const { return length_; } size_type length() const { return length_; } bool empty() const { return length_ == 0; } ... size_type max_size() const { return length_; } size_type capacity() const { return length_; }
这是几个比较简单的函数。需要这里的是data()返回的就是持有的字符串的指针,这段数据的中间也可能是存在\0的,比如size()是10,但是在第5个字符处是\0,这也是完全有可能的。这一点和普通的std::string其实也一样。
另外size()
、length()
、max_size()
、capacity()
这4个函数返回的都是length_
,他们的值是相同的。这点和std::string是不同的。
void clear() { ptr_ = NULL; length_ = 0; }
clear()也很简单,只是单纯的将两个成员变量清零而已。StringPiece是没有resize()、reserve() 函数的。
数据修改函数
BasicStringPiece& assign(const BasicStringPiece& str, size_type pos, size_type len = npos) { ptr_ = str.data() + pos; length_ = std::min(len, str.length() - pos); return *this; } void set(const value_type* data, size_type len) { ptr_ = data; length_ = len; } void set(const value_type* str) { ptr_ = str; length_ = str ? STRING_TYPE::traits_type::length(str) : 0; }
StringPiece可以从另外一个StringPiece通过assign()
或set()
函数来修改自己的数据。但是全程也都是修改自己的两个成员而已,并没有数据拷贝。
void remove_prefix(size_type n) { ptr_ += n; length_ -= n; } void remove_suffix(size_type n) { length_ -= n; }
这两个函数表面上是移除字符串的前缀或后缀中的n个字符。实际也只是做的指针和长度的修改而已。修改后缀的时候,不需要调整ptr_
。
相关的还有一个移除前后空格的trim_spaces()
函数,本质是对前面两个函数的调用。
// Remove heading and trailing spaces. void trim_spaces() { size_t nsp = 0; for (; nsp < size() && isspace(ptr_[nsp]); ++nsp) {} remove_prefix(nsp); nsp = 0; for (; nsp < size() && isspace(ptr_[size()-1-nsp]); ++nsp) {} remove_suffix(nsp); }
修改其他字符串的函数
StringPiece还支持修改其他的字符串。
// 将StringPiece指向的这段字符串覆盖到target指向的位置 void CopyToString(STRING_TYPE* target) const { internal::CopyToString(*this, target); } // 将StringPiece指向的这段字符串追加到target指向的位置 void AppendToString(STRING_TYPE* target) const { internal::AppendToString(*this, target); } // 将StringPiece指向的这段字符串从pos位置开始复制n个字符到buf开始的位置中 size_type copy(value_type* buf, size_type n, size_type pos = 0) const { return internal::copy(*this, buf, n, pos); }
这三个不一一展开介绍了。CopyToString()
本质是调用的std::string的assign()
函数。AppendToString()
本质是调用的std::string的append()
函数。copy本质调用的是memcpy()
。