C++ 如何快速實現一個容器的迭代器

来源:https://www.cnblogs.com/MasterYan576356467/archive/2023/05/19/17416086.html
-Advertisement-
Play Games

# C++ 如何快速實現一個容器的迭代器 ## 引言 C++的標準庫中的容器都會提供迭代器,如果一個容器滿足forward_range,那麼這個容器一般會提供以下成員類型和函數: - iterator - const_iterator - begin - end - begin - cend 如果該 ...


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版本的重載實現和non-const版本是完全一致的,並且我們也提供了構造函數使得iterator可以轉化為const_iterator,所以我們直接使用const_cast實現了const版本的重載。


基於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);

我們的簡化過程到此為止就結束了,剩下的部分保持不變即可。關於簡化之後的代碼,讀者可以點擊這裡獲取。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ChunJun 是⼀款穩定、易⽤、⾼效、批流⼀體的數據集成框架,基於計算引擎 Flink 實現多種異構數據源之間的數據同步與計算。ChunJun 可以把不同來源、格式、特點性質的數據在邏輯上或物理上有機地集中,從⽽為企業提供全⾯的數據共用,目前已在上千家公司部署且穩定運⾏。 在之前,我們曾經為大家介 ...
  • 2023年5月9日-5月11日,HUAWEI P60系列及旗艦產品發佈會在歐洲德國、中東非阿聯酋、亞太馬來西亞、拉美墨西哥陸續舉辦,為消費者帶來高端影像旗艦HUAWEI P60 Pro及系列全場景智能新品。其中在亞太站,還傳遞了一個重要消息:2023年6月30日之前,購買HUAWEI P60系列及折 ...
  • **本文為千鋒資深前端教學老師帶來的【JavaScript全解析】系列,文章內含豐富的代碼案例及配圖,從0到1講解JavaScript相關知識點,致力於教會每一個人學會JS!** **文末有本文重點總結,可以收藏慢慢看\~ 更多技術類內容,主頁關註一波!** # ES6函數中參數的預設值 給函數的形 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 loading的展示和取消可以說是每個前端對介面的時候都要關心的一個問題。這篇文章將要幫你解決的就是如何結合axios更加簡潔的處理loading展示與取消的邏輯。 首先在我們平時處理業務的時候loading一般分為三種:按鈕loadin ...
  • 馬上就要520了,也許你能用到這款《文生圖》工具!把字藏在圖裡,發給女神,大膽去表白吧~,文末附源碼喲!預覽地址:https://dombro.site/tools#/text-image ...
  • 當定義和調用函數時,JavaScript 函數對象會自動具有一些特定的屬性,以下是一些常見的屬性和方法。 1. arguments : arguments 是一個類數組對象,它包含了函數調用時傳遞的參數。它允許你在函數內部訪問傳遞給函數的參數列表,即使在函數定義時未明確聲明這些參數。可以通過索引訪問 ...
  • 關於JWT,可以說是分散式系統下的一個利器,我在我的很多項目實踐中,認證系統的第一選擇都是JWT。它的優勢會讓你欲罷不能,就像你領優惠券一樣。 ...
  • 有很多人問過我,學習開源項目消息推送平臺austin需要有什麼基礎,我往往會回答:**有`SpringBoot`基礎就夠了**。 我在幾年前總結過從零學習`Java`的路線,現在看來也沒有很過時: - `Java`基礎:流程式控制制-->面向對象(包括語法)-->集合-->`IO`流-->異常-->多線 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...