bRPC小课堂:从StringPiece说开来(二)

简介: 序言何谓StringPiece?StringPiece的常见使用场景源码剖析StringPieceBasicStringPiece模板构造函数容量相关函数数据修改函数修改其他字符串的函数数据访问函数比较函数查找函数截取子串返回string对象从StringPiece到string_view备胎转正API的差异如果你没有C++14/17

数据访问函数


value_type operator[](size_type i) const { return ptr_[i]; }


可以通过下标运算符[]来访问StringPiece中的单个元素(即字符),这个函数和std::string是有区别的,下面这个是std::string中的定义:


reference operator[] (size_type pos);
const_reference operator[] (size_type pos) const;


不同之一就是std::string支持const和非const两个重载,而StringPiece只有const版本。


也就是无法通过[]来修改原类型的。其二就是std::string返回的都是引用(const版本是const引用),而StringPiece返回的值类型。这也不难理解,主要目的就是让Stringpiece的[]返回的单个字符和原字符串的生命周期解耦。(StringPiece内心OS:整个字符串的生命周期都是你的了,单个字符还是我说了算吧,不然也太不安全了)

其他访问函数还有:


char front() const { return *ptr_; }
  char back() const { return *(ptr_ + length_ - 1); }
  // Return the first/last character, 0 when StringPiece is empty.
  char front_or_0() const { return length_ ? *ptr_ : '\0'; }
  char back_or_0() const { return length_ ? *(ptr_ + length_ - 1) : '\0'; }


支持front()back()直接返回首尾元素,同样是const函数,且返回的是值。和普通std::string不同。比std::string多两个函数:front_or_0()back_or_0(),他们是当字符串长度为0的时候,返回一个'\0'字符。


StringPiece也支持迭代器访问:


const_iterator begin() const { return ptr_; }
  const_iterator end() const { return ptr_ + length_; }
  const_reverse_iterator rbegin() const {
    return const_reverse_iterator(ptr_ + length_);
  }
  const_reverse_iterator rend() const {
    return const_reverse_iterator(ptr_);
  }


可以正向遍历,也可以逆向遍历。但无一例外,也都是const函数,返回const迭代器。


比较函数


比较函数分为大类,一类是类内定义的成员函数,另外一类是类外定义的比较运算符的重载函数。先说第一类:


// Does "this" start with "x"
  bool starts_with(const BasicStringPiece& x) const {
    return ((this->length_ >= x.length_) &&
            (wordmemcmp(this->ptr_, x.ptr_, x.length_) == 0));
  }
  // Does "this" end with "x"
  bool ends_with(const BasicStringPiece& x) const {
    return ((this->length_ >= x.length_) &&
            (wordmemcmp(this->ptr_ + (this->length_-x.length_),
                        x.ptr_, x.length_) == 0));
  }


这两个函数在std::string中是没有的。在定义中不难看出都是调用的wordmemcmp():


static int wordmemcmp(const value_type* p,
                        const value_type* p2,
                        size_type N) {
    return STRING_TYPE::traits_type::compare(p, p2, N);
  }


对于StringPiece而言,最终还是调用的这个C++标准中的traits函数:


char_traits::compare


对应的这个实现:


static int compare (const char_type* p, const char_type* q, size_t n) {
  while (n--) {if (!eq(*p,*q)) return lt(*p,*q)?-1:1; ++p; ++q;}
  return 0;
}


其中eq和lt分别表示如何判断两个元素(字符)是否相等以及是否小于。对于字符而言这并不难。


现在说一下第二类,在StringPiece类外主要定义了operator==operator<两个运算符。


bool operator==(const StringPiece& x, const StringPiece& y) {
  if (x.size() != y.size())
    return false;
  return StringPiece::wordmemcmp(x.data(), y.data(), x.size()) == 0;
}
inline bool operator<(const StringPiece& x, const StringPiece& y) {
  const int r = StringPiece::wordmemcmp(
      x.data(), y.data(), (x.size() < y.size() ? x.size() : y.size()));
  return ((r < 0) || ((r == 0) && (x.size() < y.size())));
}


可以看出他们还是在调用wordmemcmp()来完成比较。有了这两个函数以后,那剩下的 !=>>=<= 就能能实现了。这里不赘述了。这一部分可以看出StringPiece在进行比较的时候并没有什么新意。核心部分还是C++库中已经实现的,针对两段连续存储的字符比较的功能。


查找函数

这块相关的函数比较多,首先看两个常用的两类查找函数,正向查找find()和反向查找rfind():


// find: Search for a character or substring at a given offset.
  size_type find(const BasicStringPiece<STRING_TYPE>& s,
                 size_type pos = 0) const {
    return internal::find(*this, s, pos);
  }
  size_type find(value_type c, size_type pos = 0) const {
    return internal::find(*this, c, pos);
  }
  // rfind: Reverse find.
  size_type rfind(const BasicStringPiece& s,
                  size_type pos = BasicStringPiece::npos) const {
    return internal::rfind(*this, s, pos);
  }
  size_type rfind(value_type c, size_type pos = BasicStringPiece::npos) const {
    return internal::rfind(*this, c, pos);
  }


他们都支持查找字符串和查找单个字符。并且还能指定查找的起始位置。然后返回查询到的位置。接口定义和std::string中是相同的。实现上来看他们都是调用的internal::find()/internal::rfind(),先看find()其定义如下:


size_t find(const StringPiece& self, const StringPiece& s, size_t pos) {
  return findT(self, s, pos);
}
size_t find(const StringPiece& self, char c, size_t pos) {
  return findT(self, c, pos);
}


他们都调用的findT(),但是是不同的重载。


template<typename STR>
size_t findT(const BasicStringPiece<STR>& self,
             const BasicStringPiece<STR>& s,
             size_t pos) {
  if (pos > self.size())
    return BasicStringPiece<STR>::npos;
  typename BasicStringPiece<STR>::const_iterator result =
      std::search(self.begin() + pos, self.end(), s.begin(), s.end());
  const size_t xpos =
    static_cast<size_t>(result - self.begin());
  return xpos + s.size() <= self.size() ? xpos : BasicStringPiece<STR>::npos;
}
template<typename STR>
size_t findT(const BasicStringPiece<STR>& self,
             typename STR::value_type c,
             size_t pos) {
  if (pos >= self.size())
    return BasicStringPiece<STR>::npos;
  typename BasicStringPiece<STR>::const_iterator result =
      std::find(self.begin() + pos, self.end(), c);
  return result != self.end() ?
      static_cast<size_t>(result - self.begin()) : BasicStringPiece<STR>::npos;
}


可以看出对于查找字符串其本质是使用的STL的search()函数,而查找单个字符的时候使用的是STL的find()函数。不过对于rfind()则并不相同。它的实现如下:


template<typename STR>
size_t rfindT(const BasicStringPiece<STR>& self,
              const BasicStringPiece<STR>& s,
              size_t pos) {
  if (self.size() < s.size())
    return BasicStringPiece<STR>::npos;
  if (s.empty())
    return std::min(self.size(), pos);
  typename BasicStringPiece<STR>::const_iterator last =
      self.begin() + std::min(self.size() - s.size(), pos) + s.size();
  typename BasicStringPiece<STR>::const_iterator result =
      std::find_end(self.begin(), last, s.begin(), s.end());
  return result != last ?
      static_cast<size_t>(result - self.begin()) : BasicStringPiece<STR>::npos;
}
template<typename STR>
size_t rfindT(const BasicStringPiece<STR>& self,
              typename STR::value_type c,
              size_t pos) {
  if (self.size() == 0)
    return BasicStringPiece<STR>::npos;
  for (size_t i = std::min(pos, self.size() - 1); ;
       --i) {
    if (self.data()[i] == c)
      return i;
    if (i == 0)
      break;
  }
  return BasicStringPiece<STR>::npos;
}


对于字符串的反向查找本质的调用的STL的find_end()。而对于字符的反向查找是完全手写实现的。


另外StringPiece也支持std::string中的find_first_of()find_first_not_of()find_last_of()find_last_not_of()。这里只展开一个:


size_t find_first_of(const StringPiece& self,
                     const StringPiece& s,
                     size_t pos) {
  if (self.size() == 0 || s.size() == 0)
    return StringPiece::npos;
  // Avoid the cost of BuildLookupTable() for a single-character search.
  if (s.size() == 1)
    return find(self, s.data()[0], pos);
  bool lookup[UCHAR_MAX + 1] = { false };
  BuildLookupTable(s, lookup);
  for (size_t i = pos; i < self.size(); ++i) {
    if (lookup[static_cast<unsigned char>(self.data()[i])]) {
      return i;
    }
  }
  return StringPiece::npos;
}


其中BuildLookupTable()主要是建了一张映射表:


inline void BuildLookupTable(const StringPiece& characters_wanted,
                             bool* table) {
  const size_t length = characters_wanted.length();
  const char* const data = characters_wanted.data();
  for (size_t i = 0; i < length; ++i) {
    table[static_cast<unsigned char>(data[i])] = true;
  }
}


映射表用数组实现,数组下标就是对应字符的数值,value是是否在StringPiece中出现过。后面就是普通的遍历find了。其他的其他函数实现也是类似。


截取子串


StringPiece也有substr函数可以截取子串,其返回值也是StringPiece。


StringPiece substr(const StringPiece& self,
                   size_t pos,
                   size_t n) {
  return substrT(self, pos, n);
}
template<typename STR>
BasicStringPiece<STR> substrT(const BasicStringPiece<STR>& self,
                              size_t pos,
                              size_t n) {
  if (pos > self.size()) pos = self.size();
  if (n > self.size() - pos) n = self.size() - pos;
  return BasicStringPiece<STR>(self.data() + pos, n);
}


也是常数时间复杂度,并没有产生字符串的拷贝,仅是重新计算了数据指针和长度,并构造了一个新对象。


返回string对象


STRING_TYPE as_string() const {
    // std::string doesn't like to take a NULL pointer even with a 0 size.
    return empty() ? STRING_TYPE() : STRING_TYPE(data(), size());
  }


as_string()函数会构造出std::string类型的对象。这一步会有数据的拷贝。


从StringPiece到string_view


备胎转正


在各个C++开源项目提供了不同版本StringPiece的许多年以后,事情开始有了变化。C++14开始,string_view作为实验功能被进入到C++标准。


头文件中有std::experimental::string_view 这一新增的字符串视图类型。C++17 开始string_view顺利转正。头文件纳入标准,std::string_view类型正式进入大家的视野。


https://en.cppreference.com/w/cpp/string/basic_string_view


从Piece到View,二者不完全相同,但也很像。有点hash_map和unordered_map的意味。StringPiece是string_view的前身。进入标准以后,string_view的API和前面我提到到Chromium版StringPiece的API有一些变化。


从Piece到View,二者不完全相同,但也很像。有点hash_map和unordered_map的意味。StringPiece是string_view的前身。进入标准以后,string_view的API和前面我提到到Chromium版StringPiece的API有一些变化。


API的差异


比如string_view没有assign()clear() 以及as_string() 函数。访问函数增加了at(),和一般的string类似。


string_view有front()back(),但没有front_or_0()back_or_0()。并且front()back()返回的是 constexpr const_reference。当string_view为空的时候,此时调用front()和back()是未定义行为(UB)!这点要注意。


C++20开始string_view加入了starts_with()ends_with()。C++23中多了一个contains()可以用来查找一个const char*、string_view、char是否在当前string_view中,返回一个bool类型。而StringPiece中没有contains()


最主要的是string_view在C++的语法层面,增加了一个运算符sv。


#include <string_view>
#include <iostream>
using namespace std::literals;
int main() {
    auto print = [](std::string_view sv) {
        std::cout << "size:" << sv.size() << std::endl;
        for (char c: sv) {
            std::cout << c << std::endl;
        }
        std::cout<<"----"<<std::endl;
    };
    std::string_view s1 = "abc\0\0def";
    std::string_view s2 = "abc\0\0def"sv;
    print(s1);
    print(s2);
    print(s1.substr(1, 5));
    print(s2.substr(1, 5));
}


输出:


size:3
a
b
c
----
size:8
a
b
c
d
e
f
----
size:2
b
c
----
size:5
b
c
d
----


在""双引号表达的C风格字符作为参数,且不指定长度来构造string_view对象的时候,其表现和StringPiece一致,会默认以'\0'结尾的字符作为string_view的有效字符串。而加了sv后缀以后则能将整个字符串视为有效字符串。


如果你没有C++14/17


好了,没有享受到C++14或17编译器的小伙伴们,可以使用boost库中的

boost::string_view:


https://github.com/boostorg/utility/blob/master/include/boost/utility/string_view.hpp


boost::string_ref也可以用。但貌似后面会被弃用。另外abseil中也有一个stringview。


https://github.com/abseil/abseil-cpp/blob/master/absl/strings/string_view.h


当然为了你的项目兼容性(毕竟C++98还苟活于世!),你仍然可以继续沿用各种第三方轮子中的StringPiece,可以回头在看一下本文第一节,找到适合你的。

相关文章
|
4月前
|
人工智能 程序员
|
5月前
|
数据安全/隐私保护
杨校老师课堂之权限管理系统的核心业务逻辑接口设计
杨校老师课堂之权限管理系统的核心业务逻辑接口设计
30 0
|
6月前
|
传感器 运维 定位技术
网安预习课程硬件延伸
本篇博文是一篇关于计算机硬件、软件、网络知识和故障排查的摘要。主要内容包括: 1. 应用软件: 2. 电脑故障分析: 3. 网络介绍与分类: - 总线型网络:简单、成本低,适用于小规模网络。 - **环形网络**:中心节点故障会导致整个网络失效,常用于分布式系统。 - **星形网络**:管理和扩展方便,但中心节点至关重要。 - 网形网络:高冗余和可靠性,适合对可靠性要求高的环境。 4. 网络类型: 5. IP地址: 涵盖了计算机硬件、软件故障、网络基础和IP地址等多个IT领域的基础知识,是学习计算机科学和技术的入门内容。
|
6月前
|
存储 NoSQL API
day04—圈子功能实现
day04—圈子功能实现
48 1
|
前端开发 JavaScript Java
|
前端开发
前端学习笔记202306学习笔记第五十一天-建造模式2
前端学习笔记202306学习笔记第五十一天-建造模式2
39 0
|
前端开发
前端学习笔记202306学习笔记第五十一天-建造模式3
前端学习笔记202306学习笔记第五十一天-建造模式3
40 0
|
前端开发
前端学习笔记202306学习笔记第五十一天-建造模式1
前端学习笔记202306学习笔记第五十一天-建造模式1
51 0
|
前端开发 架构师
前端学习笔记202304学习笔记第八天-架构师的思维设计需求1
前端学习笔记202304学习笔记第八天-架构师的思维设计需求1
53 0
|
存储 域名解析 弹性计算
网站构成的基本要素| 学习笔记
快速学习网站构成的基本要素。
网站构成的基本要素| 学习笔记