brpc小课堂:从StringPiece说开来(一)

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


  • 何谓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项目中拿过来的。


640.png


可以看下该文件的开头的注释:


// 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++项目中都能见到类似的字符串工具类的身影。



其中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()



相关文章
|
2月前
|
存储 JavaScript Java
站在软件工程的角度重新思考面向对象(含高清图谱)
站在软件工程的角度重新思考面向对象(含高清图谱)
60 0
|
1月前
|
设计模式 小程序 程序员
程序员的自我修养 - 架构主题简思
对架构主题的简思汇总,可以作为日常思考主题,是程序的自我修养。
|
2月前
|
算法 测试技术
代码之美:从功能实现到艺术创造
【4月更文挑战第30天】 在编程的宇宙里,每一行代码都承载着逻辑与功能的严谨性,但在这背后,隐藏着一种创造力与美学的追求。本文将探讨如何将日复一日的编码工作转变为一种技术艺术形式,揭示代码美学的内涵,以及如何在保证功能性的同时提升代码的审美价值。
|
2月前
|
算法 程序员 UED
探索编程之道:从功能实现到艺术创造
【2月更文挑战第18天】 在数字世界的构建中,编程已不仅仅是逻辑与算法的堆砌,它正逐步演变成一种创造性表达的手段。本文将探讨编程从基础的功能实现向高级的艺术创造的转变过程,分析编程者如何通过技术深入、创新思维和持续实践,提升其技艺至艺术境界。我们将审视几个关键要素——技术的深度理解、设计的美学融入以及代码的工艺精神,并讨论它们如何共同作用于编程实践中,以培养出能够编织数字世界之美的编程艺术家。
|
11月前
|
人工智能 数据可视化 算法
9种常见思维导图种类,引领创意风暴!
除了树状图的形式,思维导图还有其它表现形式激发创意,整理思绪
9种常见思维导图种类,引领创意风暴!
|
存储 缓存 JavaScript
上帝视角:秒杀系统全貌 阅读10分钟
上帝视角:秒杀系统全貌 阅读10分钟
|
存储 域名解析 弹性计算
网站构成的基本要素| 学习笔记
快速学习网站构成的基本要素。
116 0
网站构成的基本要素| 学习笔记
|
存储 域名解析 弹性计算
网站构成基本要素|学习笔记
快速学习网站构成基本要素。
102 0
网站构成基本要素|学习笔记
|
安全 UED
开发语音直播平台,需要认清的某些概念
开发语音直播平台,需要认清的某些概念
|
自然语言处理 JavaScript 前端开发
全网最通透的“闭包”认知 · 跨越语言
今天我们深入聊一聊[闭包], 查缺补漏! 1. 以面试题 · 投石问路 2. 以C#闭包 · 庖丁解牛 3. 跨越语言 ·追本溯源 • 头等函数 •自由变量 •词法作用域 4. 答面试题 · 返璞归真
全网最通透的“闭包”认知 · 跨越语言