首页 > 编程语言 >C++ 如何快速实现一个容器的迭代器

C++ 如何快速实现一个容器的迭代器

时间:2023-05-19 19:36:02浏览次数:48  
标签:容器 const 迭代 iterator C++ LinkListIterator using return

C++ 如何快速实现一个容器的迭代器

引言

C++的标准库中的容器都会提供迭代器,如果一个容器满足forward_range,那么这个容器一般会提供以下成员类型和函数:

  • iterator
  • const_iterator
  • begin
  • end
  • begin
  • cend

如果该容器还满足bidirectional_range,那么该容器还会额外提供以下成员类型和函数:

  • reversed_iterator
  • const_reversed_iterator
  • rbegin
  • rend
  • rcbegin
  • rcend

笔者曾经在网上搜索过如何实现C++迭代器的文章,但是大多数文章都只是实现了一个最普通的迭代器,从学习原理的角度来说是非常好的,但是相比于标准库或者其他工业化的库来说可能是非常不完整的,所以笔者打算在本文中尽可能地将容器中的迭代器部分完整地实现出来。

由于可能大多数人由于种种原因,C++的标准还在C++11,所以我们这里分别使用C++11和最新的标准来编写迭代器。


什么是迭代器

我们这里引用cppreference原话:

Iterators are a generalization of pointers that allow a C++ program to work with different data structures (for example, containers and ranges (since C++20)) in a uniform manner.

从定义不难看出,迭代器是一个泛化的指针。


指针

既然迭代器是泛化的指针,那么我们可以通过指针来了解迭代器。我们知道指针共有两个位置可以添加const关键字,共有4种可能,我们将他们对应到迭代器类型:

  • iterator => T *
  • const_iterator => const T *
  • const iterator => T * const
  • const const_iterator => const T * const

对于指针类型,const在前面表示说不可以通过该指针去修改所指的变量,而const在后面则表示该指针只能指向某个变量,不能修改指针本身,但是可以修改变量。我们在实现迭代器成员类型和函数的时候只需要关注前两者即可,后面的可以按照C++正常的写法来,可以加const的地方直接加上即可。


基于C++11的具体实现

由于我们的重点在于迭代器部分,所以我们尽可能地选取一个简单的数据结构来实现容器,这里我们实现一个双链表。同时我们也假设读者阅读过关于迭代器实现的文章,对迭代器的核心实现有一定的了解。

首先定义一下链表基本的实现:

template <typename T>
class LinkList {
    
    struct ListNodeBase {

        ListNodeBase* m_next;
        ListNodeBase* m_prev;

        ListNodeBase() { reset(); }

        void reset() { m_prev = m_next = this; }
    };

    struct ListNode : ListNodeBase {
        T m_value;
    };

    // 循环链表头结点,next指向第一个元素,prev指向最后一个元素
    ListNodeBase m_header;  
}

接下来是实现迭代器部分,对于很多容器来说,iterator和const_iterator是两个不同类,但是实际上这两个类里面的实现几乎是完全一样的,这也许很容易让人想到继承的写法,但是在C++中我们还有一个更好的方法,那就是使用模板,使用模板的代码量也许会少于使用继承。如果读者是一个非常排斥使用模板的人,不妨尝试着去接受一下,毕竟模板是C++中非常重要的一部分,同时作者也相信在注释和自身基本功到位的情况下,模板的编写和维护也不是非常困难的。

template <bool Const>
struct LinkListIterator {
    // ...
};

using iterator = LinkListIterator<false>;
using const_iterator = LinkListIterator<true>;

我们使用一个bool类型的变量(Const)来让LinkListIterator在二者之间进行切换,当该变量为true时,他是const_iterator,否则是iterator。

迭代器一般会定义以下成员类型:

  • value_type
  • reference
  • pointer
  • difference_type
  • iterator_category

value_type在容器的迭代器中一般是当前容器元素的类型。
reference在容器的迭代器中一般是当前容器元素的引用类型。
pointer在容器的迭代器中一般是当前容器元素的指针类型,C++20之后无需定义。
difference_type表示两个迭代器距离的类型,在容器中一般是ptrdiff_t。
iterator_category表示该迭代器的种类。

需要注意的是,容器中的迭代器并不能够代表所有的情况,比如迭代器reference并不一定是引用类型,iterator_category也仅仅只是一个必要条件。

using value_type = typename std::conditional<Const, const T, T>::type;
using reference = value_type&;
using pointer = value_type*;   
using difference_type = std::ptrdiff_t;
using iterator_category = std::bidirectional_iterator_tag;

然后是定义构造函数:

node_type* m_ptr;

LinkListIterator() = default;

LinkListIterator(const LinkListIterator&) = default;

LinkListIterator(node_type* ptr) : m_ptr(ptr) { }

// 我们可以使用一个int* 来初始化一个const int*
// 同理,我们可以使用iterator来初始化一个const_iterator
template <bool IsConst, typename = typename std::enable_if<(Const && !IsConst)>::type>
LinkListIterator(const LinkListIterator<IsConst>& other) 
    : m_ptr(other.m_ptr) { }

C++迭代器的操作一般是基于运算符的,所以我们需要对相应的运算符进行重载。bidirectional_iterator需要重载以下操作符。

// C++11
bool operator==(const LinkListIterator& other) const {
    return m_ptr == other.m_ptr;
}

bool operator!=(const LinkListIterator& other) const {
    return !this->operator==(other);
}

reference operator*() const { return m_ptr->m_value; }

pointer operator->() const { return &this->operator*(); }

LinkListIterator& operator++() {
    m_ptr = m_ptr->m_next;
    return *this;
}

// it++
LinkListIterator operator++(int) {
    auto old = *this;
    ++*this;
    return old;
}

// --it
LinkListIterator& operator--() {
    m_ptr = m_ptr->m_prev;
    return *this;
}

// it--
LinkListIterator operator--(int) {
    auto old = *this;
    --*this;
    return old;
}

到此为止,一个双链表的迭代器就全部实现完毕了,至于reversed_iterator,标准库为我们提供了std::reverse_iterator,我们只需要将我们的迭代器传给它即可。

using iterator = LinkListIterator<false>;
using const_iterator = LinkListIterator<true>;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;

最后我们提供一下相关的成员函数:

iterator begin() { return m_header.m_next; }
iterator end() { return &m_header; }

const_iterator begin() const { return const_cast<LinkList&>(*this).begin(); }
const_iterator end() const { return const_cast<LinkList&>(*this).end(); }

const_iterator cbegin() const { return begin(); }
const_iterator cend() const { return end(); }

reverse_iterator rbegin() { return end(); }
reverse_iterator rend() { return begin(); }

const_reverse_iterator rbegin() const { return end(); }
const_reverse_iterator rend() const { return begin(); }

const_reverse_iterator rcbegin() const { return end(); }
const_reverse_iterator rcend() const { return begin(); }

为了简化操作,我们直接使用const_cast实现了const版本的重载,在UE4源码中也有许多类似的操作。


基于C++23的具体实现

随着技术的不断进步,很多时候我们不必再使用以往繁琐的方式来实现我们想要的东西。既然reverse_iterator可以直接使用标准库的组件完成,那么const_iterator是否也可以使用类似的方法来实现呢?毕竟看起来我们只需要对iterator进行简单封装就可以把它变成一个const_iterator。

答案是肯定的,那就是std::const_iterator。当然这个东西涉及的细节还是非常多的,不是看起来那么简单,读者有兴趣的话可以自行阅读相关文献。

我们来尝试简化一下上述代码:

// template <bool Const>
struct LinkListIterator {
    using value_type = T;
    using reference = value_type&;
    // using pointer = value_type*;
    using difference_type = ptrdiff_t;
    using iterator_category = std::bidirectional_iterator_tag;

    // 我们可以使用一个int* 来初始化一个const int*
    // 同理,我们可以使用iterator来初始化一个const_iterator
    // template <bool IsConst, typename = typename std::enable_if<(Const && !IsConst)>::type>
    // LinkListIterator(const LinkListIterator<IsConst>& other) 
    //     : m_ptr(other.m_ptr) { }

};

using iterator = LinkListIterator;
using const_iterator = std::const_iterator<iterator>;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;

我们只需要实现一个普通的iterator即可,因为标准库已经为我们提供了新的组件,同时额外的构造函数也自然不再需要。

pointer这一成员类型也是不必要的,std::iterator_traits会自动帮我们推导。没有pointer之后我们可以直接使用auto来完成->运算符重载。

auto operator->() const { return &this->operator*(); }

同时对于==这一运算符,许多实现无非就是简单比较每一个成员字段是否相等,而!=等于运算符往往也是等价于==运算结果取反。

bool operator==(const LinkListIterator& other) const = default;

// 不写则自动生成
// bool operator!=(const LinkListIterator& other) const { ... } 

默认生成的==运算符的,有如下规则:

A class can define operator== as defaulted, with a return value of bool. This will generate an equality comparison of each base class and member subobject, in their declaration order. Two objects are equal if the values of their base classes and members are equal. The test will short-circuit if an inequality is found in members or base classes earlier in declaration order.

需要注意的是,数组类型的比较是按照range的规则来的,即比较数组中每一个元素是否相等,而非将数组看成一个指针进行比较:

struct F
{
    int data[2];
    constexpr F(int a, int b) : data{a, b} { }
    constexpr bool operator==(const F&) const = default;
};

constexpr F f1(1, 2), f2(3, 4), f3(1, 2);

static_assert(f1 != f2);
static_assert(f1 == f3);

我们的简化过程到此为止就结束了,剩下的部分保持不变即可。关于简化之后的代码,读者可以点击这里获取。

标签:容器,const,迭代,iterator,C++,LinkListIterator,using,return
From: https://www.cnblogs.com/MasterYan576356467/p/17416086.html

相关文章

  • 详解c++STL—容器set/multiset
    1、set基本概念1.1、功能所有元素都会在插入时自动被排序1.2、本质:set/multiset属于关联式容器,底层结构是用二叉树实现。1.3、set和multiset区别set不允许容器中有重复的元素multiset允许容器中有重复的元素2、set构造和赋值2.1、功能描述创建set容器以及赋值2.1、构造set<T>st;/......
  • c++局部静态变量是线程安全的
    mark一下。c++11之前,局部静态变量初始化并不是线程安全的。c++11之后,当局部静态在初始化的过程中,有新的获取,会阻塞等待初始化成功。classInstance{public://... staticGetInstace() { staticInstanceinstance; returninstance; }};new,理论上应该也是可以的,......
  • 《C++ string类》
    1.string类常见的构造函数1)string(constchar*s):将string对象初始化为s指向的字符串stringstr("Hello!"); 2)string(size_typen,charc):创建一个包含n个元素的string对象,其中每个元素都被初始化为字符cstringstr(10,'a'); 3)string(conststring&str)......
  • C++中使用强类型的Enum Class
    在C++中,有Enumclass这种说法,在EffectivemodernC++这本书中,也提到Preferscopedenumstounscopedenum,就是说要用有范围的enumclass代替没有范围的enum.为什么会有这个问题呢?我们来看一个C++里面使用传统enum的例子:enumShape{circle,retangle};autocircle=10;......
  • 开心档之C++ Web 编程
    C++Web编程什么是CGI?公共网关接口(CGI),是一套标准,定义了信息是如何在Web服务器和客户端脚本之间进行交换的。CGI规范目前是由NCSA维护的,NCSA定义CGI如下:公共网关接口(CGI),是一种用于外部网关程序与信息服务器(如HTTP服务器)对接的接口标准。目前的版本是CGI/1.1,CGI/......
  • 开心档之C++ 变量类型
    C++变量类型变量其实只不过是程序可操作的存储区的名称。C++中每个变量都有指定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不......
  • c++函数参数和返回值
    c++函数参数和返回值函数存储位置函数参数入栈顺序初始化列表函数的返回值用参数引用来返回返回一个参数指针返回一个对象总结函数的几种变体inline函数函数对象lambda函数c++函数参数和返回值c++一直以来是一个关注效率的代码,这样关于函数的参数传递......
  • Docker容器安装示例(nginx、redis、nacos、oracle)
    1.nginx示例1.创建容器1.查看是否有nginx镜像dockerimages2.如果没有镜像,可以搜索镜像dockersearchnginx3.指定版本拉取nginxdockerpullnginx:1.20.04.查看镜像dockerimages5.创建容器(-d后台运行,-p容器80端口映射到宿主机8080端口,指定名称nginx-test,指定镜像ID:......
  • Qt C++5.9开发指南
     第1章认识Qt1.1Qt简介1、Qt是一套应用程序开发类库,但与MFC不同,Qt是跨平台开发类库。2、跨平台意味着只需要编写一次程序,在不同平台上无需改动或只是需要少许改动后再编译,就可以形成不同平台上运行的版本。1.2Qt的获取与安装1.2.1Qt的许可类型1.2.2Qt的版本1、如果不......
  • spring5中IOC容器(底层原理1-3)
    什么是IOC1.控制反转:把对象创建和对象之间的调用过程,交给spring进行管理2.使用IOC目的:为了耦合度降低IOC底层原理xml解析,工厂模式,反射 画图讲解IOC底层原理  IOC过程:  IOC接口1.IOC思想基于IOC容器完成,IOC容器底层就是对象工厂2.Spring提供IOC容......