【C++要笑着学】迭代器适配器 | 内嵌类型实现反向迭代器 | 迭代器萃取

简介: 上一章讲解 list 模拟实现时,我们简单的提到了反向迭代器,我们说反向迭代器其实就是对正向迭代器的一种封装 —— 适配器模式(配接器模式)。当时我们做的是简单的了解,本章我们就来探讨这一部分的知识。

💭 写在前面


上一章讲解 list 模拟实现时,我们简单的提到了反向迭代器,我们说反向迭代器其实就是对正向迭代器的一种封装 —— 适配器模式(配接器模式)。当时我们做的是简单的了解,本章我们就来探讨这一部分的知识。


0x00 引入:什么是适配器

上一章讲解 list 迭代器的实现时,我们提到了反向迭代器的实现。


💬 代码:reverse_iterator (我们自己实现的)

namespace chaos
{
  /* 定义反向迭代器 */
  template <class Iterator>
  class reverse_iterator {
  typedef reverse_iterator<Iterator, Ref, Ptr> self;
  public:
  reverse_iterator(Iterator it)
    :_it(it)
  {}
  Ref operator*() {
    //return *_it;
    Iterator prev = _it;
    return *--prev;
  }
  Ptr operator->() {
    return &operator*();
  }
  self& operator++() {
    --_it;
    return *this;
  }
  self& operator--() {
    ++_it;
    return *this;
  }
  bool operator!= (const self& rit) const {
    return _it != rit._it;
  }
  private:
  Iterator _it;
  };
}

❓ 思考:我们先来讨论一个问题,什么是迭代器适配器?


想了解这里的 "适配器",我们不如先去看看电源适配器,看完后再去理解可能会更好:

1f777f1d097ca68ecaefd54d0f05a830_410ca62de0884b1dba5278d716ebc564.png

(电源适配器)


【百度百科】电源适配器又叫外置电源,是小型便携式电子设备及电子电器的供电电压变换设备,常见于手机、液晶显示器和笔记本电脑等小型电子产品上。


电源适配器是进行 "转换" 的,它本质上可以理解为是一个变压器。


标准家庭用电电压为 ,我们设备用电其实并不需要这么高,


而电源适配器可以使较高的交流电压降低到合适于手机电池的直流工作电压。


也就是说,电源适配器是用来 "转换" 的。


 👉  


一样的道理,上一章我们提到过,反向迭代器和正向迭代器非常像!


🔍 区别: 反向迭代器和正向迭代器加加的方向不一样(唯一区别)。


(即正向迭代器 ++ 是向后走,反向迭代器 ++ 是向前走)


基于这一系列的原因,在实现反向迭代器的时,没有必要像正向迭代器这样去包装并重新实现。


💬 举个例子:


我们可以对其进行封装地 "转换" ,比如我们需要 list 的反向迭代器,


我们将 list 的正向迭代器传给 Iterator,来实例化 Iterator。


然后通过包装转换出你需要的 list 的反向迭代器:

namespace chaos
{
  /* 反向迭代器 */
  template <class Iterator>
  class reverse_iterator {
  typedef reverse_iterator<Iterator> self;
  public:
  reverse_iterator(Iterator it)
    : _it(it) 
  {}

如果你 vector 需要反向迭代器,那就把 vector 的正向迭代器传给 Iterator,


它就可以通过正向迭代器转换出 vector 的反向迭代器。


也就是说,我们实现的反向迭代器并包装的这个类,不是针对某个容器而是针对所有容器的。


任何一个容器只要你实现了正向迭代器,就可以通过其适配出反向迭代器。


它的本质是一种复用,是一种适配与转换。这就是迭代器适配器!


0x01 反向迭代器的错位访问

上一章我们提到过 operator* 这里返回的是*--prev,是一种有意的 "错位" 。


它解引用取的不是当前位置,而是前一个位置。


对于 list 而言,正向迭代器的 begin 和 end 位置如下:

593fb3ba799cd17535d926496af7e523_1f2973476c5e44a9ab4eedeeeb4057d5.png

那我们的反向迭代器的 rbegin 和 rend 在哪呢?按我们的常理来说应该是这样的:

12db38bfb35a1c02171fd6c31eeda7c8_dbc6c719316b48abb0b7225c8c7ea650.png



rbegin 和 rend 是和 end 和 begin 是相对称的形式设计的,


你的 end 就是我的 rbegin,你的 begin 就是我的 rend。


这时就产生了一个错位的问题,我们解引用的时候会对不上,举个例子:

3e43d685dac22e9f842ca239611ba0cf_7ecf0b7e0ddb4b9693aac0ff4d6dfb88.png

rit 在  rbegin() 的位置,rbegin 在头结点处,解引用取该位置么?


不,我们是反向迭代器啊,我们要取的是最后一个位置!取头结点干啥……

0e5c8d9b95c63239d7398a5050cd11fe_9c28d63486394a94bd6119c159f3b138.png


💡 解决方案:为了解决该问题,我们在 operator* 上动手脚,让其返回 *--prev:


3dbe19db8f0eb250499039a114f1201d_6689abb9c0a94880b90345d5b5fc10a1.png


这样取的就是前一个位置了,rbegin() 位置的前一个位置正是我们想要取的地方!

e2763e0cbfc5d847d50bb0f1f96816b9_c9afec7c6cc34b0e8983867c87a72f6d.png

0x02 利用内嵌类型实现反向迭代器

实际上,STL3.0 的反向迭代器只有一个模板参数:


而我们上一章模拟实现 list 的反向迭代器时,设计了 3 个模板参数

4c2053851c0ed5bcaeb672de1d3761b4_9eab984c65bc47cbb50f4dc7936fcb12.png

这是因为在上一章为了更方便地区讲解 list 的模拟实现,我们将其简化了。


库里面的实在是让人头皮发麻,我们现在来学习下库里面是如何实现的:

ae7a2d06af80975d7a84365d67f70ccf_511860ac59b74268a47b1a216d3b5a8a.png

它没有传 Ref 和 Ptr,也就意味着不能显式地去传,自己去控制(详细请阅读上一章内容)。


比如,如果你是普通迭代器的反向迭代器,传的就是 T& 和 T* :

5f35ea32cd4472092bed8432a28d315b_dc450542c47c4c8d91333adb739c504c.png

如果你是 const 迭代器的反向迭代器,传的就是 const T& 和 const T* :

701530a6938f5e62cf21a477b14a6c61_81156588b0d1433590826b20d8c851ec.png

我们这么做是为了更清楚地表示,降低学习的成本。


而库中采用了一种更难的方式去实现,不传 Ref 和 Ptr 这两个模板参数,只用了一个模板参数。


这就意味着要去取 pointer 和 reference,因为它们要做 operator* 和 operator-> 的返回值。


❓ 而这里只有迭代器 Iterator,我们该怎么去取呢?

d9fdc2ce943859a6465d99df07e8089f_5b635230280f40ffa040a7f63121821b.png

💡 大哥是这么做的,他在其中定义了一个内嵌类型:

672478746b2a9ada59c3b4054f626c12_01e708316c6b4e1fb5e1e1f9f9185e60.png

把 Ptr 和 Ref 定义成了 pointer 和 reference,这样我们就可以从内嵌类型中去取这个类型了。


💬 代码:不传 Ptr 和 Ref 这两个模板参数去实现反向迭代器

namespace chaos
{
  //template <class Iterator, class Ref, class Ptr>
  template <class Iterator>
  class reverse_iterator {
  typedef reverse_iterator<Iterator, Ref, Ptr> self;
  typedef Iterator::Ref reference;  // 取内嵌类型
  typedef Iterator::Ptr pointer;
  public:
  reverse_iterator(Iterator it)
    :_it(it)
  {}
    ...
    }
}

如果你自己试一试你就会发现一个问题:


Iterator 是一个模板参数,你要取模板参数中的内嵌类型 Ref,编译器这里就编不过去了。


如果你对模板参数取 typedef,没有任何问题。但是你要取它的内嵌类型,就有问题了:

a839c4df45967870d76da8dccf55cfc5_b471669258f146c6ab6811a5254e80eb.png

因为编译器编译到这一块的时候,模板参数还没有实例化!


既然没有实例化,那编译器也不知道这个 Iterator 的类型是什么,typedef 一下没什么问题。


但是你去 : : 取 Iterator 里面的东西,模板都还没实例化,编译器怎么知道里面到底是什么呢?


那我们该怎么让编译器知道呢?


凡是要取一个模板、类模板或模板参数的内嵌类型(内部类),就可以用 typename:

8129229c8701916a56c5048343289382_2ca874138d1b4f90b066e30ec4b7f67c.png


对于现阶段而言,这种写法是有缺陷的。vector、string 的迭代器怎么适配?


这里在取内嵌类型,只有自定义类型才能在里面搞内嵌类型。如果是一个原生指针呢?


原生指针哪来的内嵌类型?这里不就抓瞎了么。


如果仅看这一部分的实现,自然还是三个模板参数更爽,自己控制。


但是 STL 是非常复杂的,它还要考虑去兼顾全局,不是说只有迭代器和容器,


像 vector、string 这样的容器的迭代器是原生指针,无法取内嵌类型。那么上面的方式就寄了。


0x03 浅谈迭代器萃取

基于这一系列的原因,STL 源码中使用了一个叫 "迭代器萃取" 的技术解决的。

071c8fbdab4977ef0be312155e27fa6d_faff7854126246e6a8cc5fc2635ad62d.png

📚 原理:再套一层,去萃取迭代器中的 reference 和 pointer(迭代器管理的数据中的 T& 和 T* 或 const T* 和 constT& 等取出来)。相当于把其他容器的迭代器传给 iterator_traits,在其中再套一层,对于链表而言最终还是要取:

3c39fa8afdabc611b5a472067a3d316a_6053ae251be04338a5efd4eed93b6fc6.png

对于 vector、string 这样的原生指针,iterator_traits 里有使用了一个技术 —— "模板特化技术"


大概意思是当你这里是原生指针时,这里会直接自己套一层取解决。萃取的本质就是特化。


(这里先了解一下,关于这里的深入实现我们后面模板的进阶章节会详细讲解)


(迭代器萃取的知识难度较大,我们后期再专门开一个章节去讲)


🔺 总结:list 用一个参数实现还行,但是 vector 和 string 这样的原生指针还是用一个参数去实现会牵扯到更复杂的迭代器萃取。就目前而言,学会实现 3 个模板参数的反向迭代器即可。



相关文章
|
1月前
|
存储 设计模式 C++
【C++】优先级队列(容器适配器)
本文介绍了C++ STL中的线性容器及其适配器,包括栈、队列和优先队列的设计与实现。详细解析了`deque`的特点和存储结构,以及如何利用`deque`实现栈、队列和优先队列。通过自定义命名空间和类模板,展示了如何模拟实现这些容器适配器,重点讲解了优先队列的内部机制,如堆的构建与维护方法。
36 0
|
2月前
|
存储 编译器 程序员
C++类型参数化
【10月更文挑战第1天】在 C++ 中,模板是实现类型参数化的主要工具,用于编写能处理多种数据类型的代码。模板分为函数模板和类模板。函数模板以 `template` 关键字定义,允许使用任意类型参数 `T`,并在调用时自动推导具体类型。类模板则定义泛型类,如动态数组,可在实例化时指定具体类型。模板还支持特化,为特定类型提供定制实现。模板在编译时实例化,需放置在头文件中以确保编译器可见。
40 11
|
3月前
|
C++ 内存技术
[转]Visual C++内嵌swf文件并播放
[转]Visual C++内嵌swf文件并播放
|
2月前
|
设计模式 存储 C++
【C++】C++ STL探索:容器适配器 Stack 与 Queue 的使用及模拟实现(二)
【C++】C++ STL探索:容器适配器 Stack 与 Queue 的使用及模拟实现
|
2月前
|
存储 C++ 容器
【C++】C++ STL探索:容器适配器 Stack 与 Queue 的使用及模拟实现(一)
【C++】C++ STL探索:容器适配器 Stack 与 Queue 的使用及模拟实现
|
4月前
|
C++ 容器
【C/C++笔记】迭代器
【C/C++笔记】迭代器
30 1
|
3月前
|
安全 程序员 C语言
C++(四)类型强转
本文详细介绍了C++中的四种类型强制转换:`static_cast`、`reinterpret_cast`、`const_cast`和`dynamic_cast`。每种转换都有其特定用途和适用场景,如`static_cast`用于相关类型间的显式转换,`reinterpret_cast`用于低层内存布局操作,`const_cast`用于添加或移除`const`限定符,而`dynamic_cast`则用于运行时的类型检查和转换。通过具体示例展示了如何正确使用这四种转换操作符,帮助开发者更好地理解和掌握C++中的类型转换机制。
|
4月前
|
C++
使用 QML 类型系统注册 C++ 类型
使用 QML 类型系统注册 C++ 类型
101 0
|
4月前
|
存储 安全 程序员
【C/C++笔记】迭代器范围
【C/C++笔记】迭代器范围
74 0
|
4月前
|
存储 C++
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
【C/C++学习笔记】string 类型的输入操作符和 getline 函数分别如何处理空白字符
55 0