迭代器与范围

在本章中,您将学到:

  • C++ 如何以及为何将指针的概念泛化以创建迭代器概念
  • 范围在 C++ 中的重要性,以及如何用一对迭代器表示半开放范围的标准方式
  • 如何编写自己的稳固、符合常量正确性的迭代器类型
  • 如何编写作用于迭代器对的泛型算法
  • 标准迭代器层次结构及其在算法中的重要性

整数索引的问题

在前一章中,我们实现了几个作用于容器的泛型算法。再次考虑其中一个算法:

    template<typename Container>
    void double_each_element(Container& arr) 
    {
      for (int i=0; i < arr.size(); ++i) {
        arr.at(i) *= 2;
      }
    }

这个算法是根据较低层次的操作 .size().at() 定义的。对于像 array_of_intsstd::vector 这样的容器类型来说,这种方式效果还不错,但对于例如上一章中的 list_of_ints 这样的链表来说,效果就不那么好了:

    class list_of_ints {
      struct node {
        int data;
        node *next;
      };
      node *head_ = nullptr;
      node *tail_ = nullptr;
      int size_ = 0;
    public:
      int size() const { return size_; }
      int& at(int i) {
        if (i >= size_) throw std::out_of_range("at");
        node *p = head_;
        for (int j=0; j < i; ++j) {
          p = p->next;
        }
        return p->data;
      }
      void push_back(int value) {
        node *new_tail = new node{value, nullptr};
        if (tail_) {
          tail_->next = new_tail;
        } else {
          head_ = new_tail;
        }
        tail_ = new_tail;
        size_ += 1;
      }
      ~list_of_ints() {
        for (node *next, *p = head_; p != nullptr; p = next) {
          next = p->next;
          delete p;
        }
      }
    };

list_of_ints::at() 的实现是 O(n) 长度的列表——我们的列表越长,at() 就越慢。特别是,当我们的 count_if 函数遍历列表的每个元素时,它每次都会调用那个 at() 函数,这使得我们的泛型算法的运行时间成为 O(n^2)——对于一个本应是 O(n) 的简单计数操作来说!

结果表明,使用 .at() 进行整数索引并不是构建算法城堡的良好基础。我们应该选择一个更接近计算机实际操作数据的基本操作。

超越指针

在没有任何抽象的情况下,通常如何标识数组、链表或树的元素?最直接的方式是使用指向元素内存地址的指针。下面是一些指向各种数据结构元素的指针示例:

img

要在数组上进行迭代,我们只需要指针;我们可以通过从指向第一个元素的指针开始,并简单地递增该指针直到达到最后一个元素,来处理数组中的所有元素。在C语言中:

    for (node *p = lst.head_; p != nullptr; p = p->next) {
      if (pred(p->data)) {
        sum += 1;
      }
   }

但为了有效地遍历链表,我们需要的不仅仅是原始指针;递增类型为 node* 的指针极不可能产生指向链表中下一个节点的指针!在这种情况下,我们需要某种类似于指针的东西——特别是,我们应该能够解引用它来检索或修改指向的元素——但它具有与递增这一抽象概念相关的特殊、特定于容器的行为。

在C++中,鉴于我们的语言内置了操作符重载,当我说“将特殊行为与递增概念关联起来”时,你应该在想“让我们重载 ++ 操作符”。事实上,这正是我们将要做的:

    struct list_node {
      int data;
      list_node *next;
    };

    class list_of_ints_iterator {
      list_node *ptr_;

      friend class list_of_ints;
      explicit list_of_ints_iterator(list_node *p) : ptr_(p) {}
    public:
      int& operator*() const { return ptr_->data; }
      list_of_ints_iterator& operator++() { ptr_ = ptr_->next; return *this; }
      list_of_ints_iterator operator++(int) { auto it = *this; ++*this; return it; }
      bool operator==(const list_of_ints_iterator& rhs) const
        { return ptr_ == rhs.ptr_; }
      bool operator!=(const list_of_ints_iterator& rhs) const
        { return ptr_ != rhs.ptr_; }
    };

    class list_of_ints {
      list_node *head_ = nullptr;
      list_node *tail_ = nullptr;
      // ...
    public:
      using iterator = list_of_ints_iterator;
      iterator begin() { return iterator{head_}; }
      iterator end() { return iterator{nullptr}; }
    }; 

    template<class Container, class Predicate>
    int count_if(Container& ctr, Predicate pred)
    {
      int sum = 0;
      for (auto it = ctr.begin(); it != ctr.end(); ++it) {
        if (pred(*it)) {
            sum += 1;
        }
      }
      return sum;
   }

请注意,我们还重载了一元 * 操作符(用于解引用)以及 == 和 != 操作符;我们的 count_if 模板要求循环控制变量 it 支持所有这些操作。(好吧,严格来说,我们的 count_if 并不需要 == 操作;但如果你要重载一个比较操作符,你也应该重载另一个。)

常量迭代器

在我们放弃这个链表迭代器示例之前,还有一个复杂性需要考虑。请注意,我悄悄地改变了我们的 count_if 函数模板,使其接受 Container& 而不是 const Container&!这是因为我们提供的 begin() 和 end() 成员函数是非常量成员函数;这是因为它们返回的迭代器的 operator* 返回列表元素的非常量引用。我们希望使我们的列表类型(及其迭代器)完全符合常量正确性——也就是说,我们希望您能够定义并使用 const list_of_ints 类型的变量,但阻止您修改 const 列表的元素。

标准库通常通过为每个标准容器提供两种不同类型的迭代器来处理这个问题:bag::iterator 和 bag::const_iterator。非常量成员函数 bag::begin() 返回一个迭代器,而 bag::begin() const 成员函数返回一个 const_iterator。下划线非常重要!请注意,bag::begin() const 并不返回一个简单的 const 迭代器;如果返回的对象是 const,我们就不允许对其进行 ++ 操作。(反过来,这将使得在 const bag 上进行迭代变得非常困难!)不,bag::begin() const 返回的是更微妙的东西:一个非常量 const_iterator 对象,其 operator* 简单地产生指向其元素的常量引用。

也许一个例子会有所帮助。让我们继续为我们的 list_of_ints 容器实现 const_iterator。

由于 const_iterator 类型的大部分代码将与 iterator 类型的代码完全相同,我们的第一反应可能是剪切和粘贴。但这是 C++!当我说“这部分代码将与另一部分代码完全相同”时,你应该在想“让我们将共同部分制作成模板。”事实上,这正是我们要做的:

    struct list_node {
      int data;
      list_node *next;
    };

    template<bool Const>
    class list_of_ints_iterator {
      friend class list_of_ints;
      friend class list_of_ints_iterator<!Const>;

      using node_pointer = std::conditional_t<Const, const list_node*, list_node*>;
      using reference = std::conditional_t<Const, const int&, int&>;

      node_pointer ptr_;

      explicit list_of_ints_iterator(node_pointer p) : ptr_(p) {}
    public:
      reference operator*() const { return ptr_->data; }
      auto& operator++() { ptr_ = ptr_->next; return *this; }
      auto operator++(int) { auto result = *this; ++*this; return result; }

      // Support comparison between iterator and const_iterator types
      template<bool R>
      bool operator==(const list_of_ints_iterator<R>& rhs) const
        { return ptr_ == rhs.ptr_; }

      template<bool R>
      bool operator!=(const list_of_ints_iterator<R>& rhs) const
        { return ptr_ != rhs.ptr_; }

      // Support implicit conversion of iterator to const_iterator
      // (but not vice versa)
      operator list_of_ints_iterator<true>() const
        { return list_of_ints_iterator<true>{ptr_}; }
    };

    class list_of_ints {
      list_node *head_ = nullptr;
      list_node *tail_ = nullptr;
      // ...
    public:
      using const_iterator = list_of_ints_iterator<true>;
      using iterator = list_of_ints_iterator<false>;

      iterator begin() { return iterator{head_}; }
      iterator end() { return iterator{nullptr}; }
      const_iterator begin() const { return const_iterator{head_}; }
      const_iterator end() const { return const_iterator{nullptr}; }
    };
 

前述代码实现了针对我们的 list_of_ints 的完全符合常量正确性的迭代器类型。

一对迭代器定义了一个范围

现在我们理解了迭代器的基本概念,让我们将其应用于一些实际用途。我们已经看到,如果你有一对从 begin() 和 end() 返回的迭代器,你可以使用 for 循环遍历底层容器的所有元素。但更强大的是,你可以使用任何一对迭代器来遍历容器元素的任何子范围!假设你只想查看一个向量的前半部分:

    template<class Iterator>
    void double_each_element(Iterator begin, Iterator end) 
    {
      for (auto it = begin; it != end; ++it) {
        *it *= 2;
      } 
    }

    int main() 
    {
      std::vector<int> v {1, 2, 3, 4, 5, 6};
      double_each_element(v.begin(), v.end());
        // 加倍整个向量中的每个元素
      double_each_element(v.begin(), v.begin()+3);
        // 加倍向量前半部分的每个元素
      double_each_element(&v[0], &v[3]);
        // 同样加倍向量前半部分的每个元素
    }
 

请注意,在 main() 中的第一和第二个测试用例中,我们传入了从 v.begin() 派生的一对迭代器;也就是说,两个类型为 std::vector::iterator 的值。在第三个测试用例中,我们传入了两个类型为 int* 的值。由于 int* 在这种情况下满足迭代器类型的所有要求——即:可递增、可比较且可解引用——我们的代码即使使用指针也能正常工作!这个例子展示了迭代器对模型的灵活性。(然而,一般而言,如果你正在使用像 std::vector 这样提供适当迭代器类型的容器,你应该避免使用原始指针。改用从 begin() 和 end() 派生的迭代器。)

我们可以说,一对迭代器隐式地定义了一个数据元素范围。对于惊人地大的算法家族来说,这就足够了!我们不需要访问容器就能执行某些搜索或转换;我们只需要访问正在被搜索或转换的特定范围的元素。沿着这条思路进一步深入,最终会引导我们到非拥有视图的概念(这对于数据序列来说,就像 C++ 引用对于单个变量一样),但视图和范围还是更现代的概念,我们应该先完成对 1998 年版 STL 的讨论,然后再谈论这些事情。

在前面的代码示例中,我们看到了第一个真正的 STL 风格通用算法示例。诚然,double_each_element 并不是一个非常通用的算法,因为它实现的行为我们可能想在其他程序中重用;但这个函数的版本现在完全通用了,因为它只操作一对迭代器对,其中 Iterator 可以是任何实现了可递增、可比较和可解引用性的类型。(在本书的下一章中,当我们讨论 std::transform 时,我们会看到这个算法更通用的版本。)

迭代器类别

经典多态和泛型编程中介绍的 count 和 count_if 函数。将下一个示例中的函数模板定义与该章中的类似代码进行比较;你会看到除了将 Container& 参数替换为一对迭代器(也就是隐式定义的范围)外,它们是完全相同的——除了我将第一个函数的名称从 count 更改为 distance。这是因为你可以在标准模板库中几乎找到与此处描述的完全相同的函数,名为 std::distance,第二个函数名为 std::count_if:

    template<typename Iterator>
    int distance(Iterator begin, Iterator end) 
    {
      int sum = 0;
      for (auto it = begin; it != end; ++it) {
        sum += 1;
      }
      return sum;
    }

    template<typename Iterator, typename Predicate>
    int count_if(Iterator begin, Iterator end, Predicate pred) 
    {
      int sum = 0;
      for (auto it = begin; it != end; ++it) {
        if (pred(*it)) {
            sum += 1;
        }
      }
      return sum; 
    }

    void test() 
    {
      std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

      int number_above = count_if(v.begin(), v.end(), [](int e) { return e > 5; });
      int number_below = count_if(v.begin(), v.end(), [](int e) { return e < 5; });

      int total = distance(v.begin(), v.end()); // 可疑 

      assert(number_above == 2);
      assert(number_below == 5);
      assert(total == 8);
    }
 

但让我们考虑一下上例中标记为“可疑”的那一行。在这里,我们通过反复递增一个迭代器直到它达到另一个迭代器来计算两个迭代器之间的距离。这种方法的性能如何?对于某些类型的迭代器——例如,list_of_ints::iterator——我们不可能做得比这更好。但对于 vector::iterator 或 int* 这样的迭代器来说,它们在连续数据上进行迭代,如果我们使用一个 O(n) 算法的循环来实现,而不是通过简单的指针减法在 O(1) 时间内实现同样的事情,那就有点愚蠢了。也就是说,我们希望标准库中的 std::distance 包含类似下面的模板特化:

    template<typename Iterator>
    int distance(Iterator begin, Iterator end)
    {
      int sum = 0;
      for (auto it = begin; it != end; ++it) {
        sum += 1;
      }
      return sum;
    }

    template<> 
    int distance(int *begin, int *end) 
    {
      return end - begin;
    }
 

但我们不希望特化仅存在于 int* 和 std::vector::iterator。我们希望标准库的 std::distance 对所有支持这个特定操作的迭代器类型都是高效的。这意味着,我们开始形成一种直觉:至少存在两种不同类型的迭代器:一种是可递增、可比较和可解引用的;另一种则是可递增、可比较、可解引用并且可相减的!事实证明,对于任何支持 i = p - q 操作的迭代器类型,其逆操作 q = p + i 也是有意义的。支持减法和加法的迭代器被称为随机访问迭代器

因此,标准库的 std::distance 应对随机访问迭代器和其他类型的迭代器都高效。为了更容易地提供这些模板的部分特化,标准库引入了迭代器种类层次结构的概念。支持加法和减法的迭代器,如 int*,被称为随机访问迭代器。我们会说它们满足 RandomAccessIterator 概念。

略微弱于随机访问迭代器的迭代器可能不支持任意距离的加法或减法,但至少支持使用 ++p 和 --p 的递增和递减。这种性质的迭代器被称为 BidirectionalIterator。所有 RandomAccessIterator 都是 BidirectionalIterator,但反之则不必然。在某种意义上,我们可以想象 RandomAccessIterator 是相对于 BidirectionalIterator 的子类或子概念;我们可以说 BidirectionalIterator 是一个较弱的概念,相较于 RandomAccessIterator,它施加的要求更少。

更弱的概念是那些甚至不支持递减的迭代器。例如,我们的 list_of_ints::iterator 类型不支持递减,因为我们的链表没有前向指针;一旦你有了一个指向列表给定元素的迭代器,你只能向前移动到后面的元素,永远不能回到前一个元素。支持 ++p 但不支持 --p 的迭代器被称为 ForwardIterator。ForwardIterator 是比 BidirectionalIterator 更弱的概念。

输入和输出迭代器

我们甚至可以想象比 ForwardIterator 更弱的概念!例如,你可以用 ForwardIterator 做的一件有用的事情是复制它,保存副本,并使用它两次迭代同一数据。操作迭代器(或其副本)根本不影响底层数据范围。但我们可以想象一个如下代码片段中的迭代器,其中根本没有底层数据,甚至复制迭代器都没有意义:

    class getc_iterator {
      char ch;
    public:
      getc_iterator() : ch(getc(stdin)) {}
      char operator*() const { return ch; }
      auto& operator++() { ch = getc(stdin); return *this; }
      auto operator++(int) { auto result(*this); ++*this; return result; }
      bool operator==(const getc_iterator&) const { return false; }
      bool operator!=(const getc_iterator&) const { return true; }
    };

(实际上,标准库中包含一些非常类似于这个的迭代器类型;我们将在第 9 章 Iostreams中讨论其中一种类型,std::istream_iterator。)这样的迭代器,它们不具有有意义的可复制性,且实际上并不指向任何有意义的数据元素,被称为 InputIterator 类型。

镜像案例也是可能的。考虑以下发明的迭代器类型:

    class putc_iterator {
      struct proxy {
        void operator= (char ch) { putc(ch, stdout); }
      };
    public:
      proxy operator*() const { return proxy{}; }
      auto& operator++() { return *this; }
      auto& operator++(int) { return *this; }
      bool operator==(const putc_iterator&) const { return false; }
      bool operator!=(const putc_iterator&) const { return true; }
    };

    void test()
    {
      putc_iterator it;
      for (char ch : {'h', 'e', 'l', 'l', 'o', '\n'}) {
        *it++ = ch;
      }
    }

这样的迭代器,它们不具有有意义的可复制性,并且是可写入但不可读出的,被称为 OutputIterator 类型。

C++ 中的每种迭代器类型至少属于以下五个类别之一:

  • InputIterator
  • OutputIterator
  • ForwardIterator
  • BidirectionalIterator
  • RandomAccessIterator

请注意,虽然很容易在编译时判断一个特定的迭代器类型是否符合 BidirectionalIterator 或 RandomAccessIterator 的要求,但仅从它支持的语法操作来判断,我们无法确定我们是在处理一个 InputIterator、一个 OutputIterator 还是一个 ForwardIterator。就刚才的例子而言,考虑:getc_iterator、putc_iterator 和 list_of_ints::iterator 支持完全相同的语法操作——用 *it 解引用,用 ++it 递增,用 it != it 比较。这三个类只在语义层面上有所不同。那么标准库如何区分它们呢?

事实证明,标准库需要来自每种新迭代器类型的实现者的一点帮助。标准库的算法只会与定义了名为 iterator_category 的成员 typedef 的迭代器类一起工作。也就是说:

    class getc_iterator {
      char ch;
    public:
      using iterator_category = std::input_iterator_tag;

      // ...
    };

    class putc_iterator {
      struct proxy {
        void operator= (char ch) { putc(ch, stdout); }
      };
    public:
      using iterator_category = std::output_iterator_tag;

      // ...
    };

    template<bool Const>
    class list_of_ints_iterator {
      using node_pointer = std::conditional_t<Const, const list_node*,
       list_node*>;
      node_pointer ptr_;

    public:
      using iterator_category = std::forward_iterator_tag;

      // ...
    };
 

那么任何标准(或者非标准)算法,如果想根据其模板类型参数的迭代器类别来定制其行为,都可以通过检查这些类型的 iterator_category 来简单地实现这种定制。

前段所述的迭代器类别,对应于在 <iterator> 头文件中定义的以下五个标准标签类型:

    struct input_iterator_tag { };
    struct output_iterator_tag { };
    struct forward_iterator_tag : public input_iterator_tag { };
    struct bidirectional_iterator_tag : public forward_iterator_tag { };
    struct random_access_iterator_tag : public bidirectional_iterator_tag
    { };

请注意,random_access_iterator_tag 实际上是从 bidirectional_iterator_tag 派生的(在经典的 OO、多态类层次结构的意义上),以此类推:迭代器种类的概念层次结构反映在 iterator_category 标签类的类层次结构中。这在你进行标签分派的模板元编程时非常有用;但你只需要知道,如果你想向函数传递一个 iterator_category,那么类型为 random_access_iterator_tag 的标签会匹配期望类型为 bidirectional_iterator_tag 的函数参数:

    void foo(std::bidirectional_iterator_tag t [[maybe_unused]])
    {
      puts("std::vector 的迭代器确实是双向的..."); 
    }

    void bar(std::random_access_iterator_tag)
    {
      puts("...而且也是随机访问的!");
    }

    void bar(std::forward_iterator_tag)
    {
      puts("forward_iterator_tag 不是那么好的匹配");
    }

    void test()
    {
      using It = std::vector<int>::iterator;
      foo(It::iterator_category{});
      bar(It::iterator_category{});
    }
 

此时,我想你可能在想:“但是 int* 呢?我们怎样为根本不是类类型,而是原始标量类型的东西提供成员 typedef 呢?标量类型不能有成员 typedef。”好吧,像大多数软件工程中的问题一样,这个问题可以通过增加一层间接性来解决。标准算法不是直接引用 T::iterator_category,而是总是引用 std::iterator_traits::iterator_category。当 T 是指针类型时,类模板 std::iterator_traits 被适当地特化。

此外,std::iterator_traits 被证明是挂载其他成员 typedef 的方便之处。它提供以下五个成员 typedef,仅当 T 本身提供所有五个(或者 T 是指针类型)时:

iterator_category, difference_type, value_type, pointer, 和 reference。

总结一切

综合我们在这章所学的内容,我们现在可以编写如下示例代码。在这个例子中,我们实现了我们自己的 list_of_ints 以及我们自己的迭代器类(包括一个符合常量正确性的 const_iterator 版本);我们通过提供五个重要的成员 typedef,使其能够与标准库一起工作。

    struct list_node {
      int data;
      list_node *next;
    };

    template<bool Const>
    class list_of_ints_iterator {
      friend class list_of_ints;
      friend class list_of_ints_iterator<!Const>;

      using node_pointer = std::conditional_t<Const, const list_node*,
        list_node*>;
      node_pointer ptr_;

      explicit list_of_ints_iterator(node_pointer p) : ptr_(p) {}
    public:
      // Member typedefs required by std::iterator_traits
      using difference_type = std::ptrdiff_t;
      using value_type = int;
      using pointer = std::conditional_t<Const, const int*, int*>;
      using reference = std::conditional_t<Const, const int&, int&>;
      using iterator_category = std::forward_iterator_tag;

      reference operator*() const { return ptr_->data; }
      auto& operator++() { ptr_ = ptr_->next; return *this; }
      auto operator++(int) { auto result = *this; ++*this; return result; }

      // Support comparison between iterator and const_iterator types
      template<bool R>
      bool operator==(const list_of_ints_iterator<R>& rhs) const
        { return ptr_ == rhs.ptr_; }

      template<bool R>
      bool operator!=(const list_of_ints_iterator<R>& rhs) const
        { return ptr_ != rhs.ptr_; }

      // Support implicit conversion of iterator to const_iterator
      // (but not vice versa)
      operator list_of_ints_iterator<true>() const { return 
        list_of_ints_iterator<true>{ptr_}; }
    };

    class list_of_ints {
      list_node *head_ = nullptr;
      list_node *tail_ = nullptr;
      int size_ = 0;
    public:
      using const_iterator = list_of_ints_iterator<true>;
      using iterator = list_of_ints_iterator<false>;

      // Begin and end member functions
      iterator begin() { return iterator{head_}; }
      iterator end() { return iterator{nullptr}; }
      const_iterator begin() const { return const_iterator{head_}; }
      const_iterator end() const { return const_iterator{nullptr}; }

      // Other member operations
      int size() const { return size_; }
      void push_back(int value) {
        list_node *new_tail = new list_node{value, nullptr};
        if (tail_) {
          tail_->next = new_tail;
        } else {
          head_ = new_tail;
        }
        tail_ = new_tail;
        size_ += 1;
      }
      ~list_of_ints() {
        for (list_node *next, *p = head_; p != nullptr; p = next) {
          next = p->next;
          delete p;
        }
      }
    };
 

接下来,为了展示我们理解标准库是如何实现泛型算法的,我们将完全按照 C++17 标准库的方式实现函数模板 distance 和 count_if。

注意在 distance 中使用了 C++17 的新语法 if constexpr。虽然我们在这本书中不会过多地讨论 C++17 的核心语言特性,但可以肯定地说,相比于你在 C++14 中不得不编写的许多笨拙的样板代码,你可以使用 if constexpr 来消除很多不便。

    template<typename Iterator>
    auto distance(Iterator begin, Iterator end)
    {
      using Traits = std::iterator_traits<Iterator>;
      if constexpr (std::is_base_of_v<std::random_access_iterator_tag,
        typename Traits::iterator_category>) {
          return (end - begin);
        } else {
         auto result = typename Traits::difference_type{};
         for (auto it = begin; it != end; ++it) {
           ++result;
         }
         return result;
      }
    }

    template<typename Iterator, typename Predicate>
    auto count_if(Iterator begin, Iterator end, Predicate pred) 
    {
      using Traits = std::iterator_traits<Iterator>;
      auto sum = typename Traits::difference_type{};
      for (auto it = begin; it != end; ++it) {
        if (pred(*it)) {
          ++sum;
        }
      }
      return sum;
    }

    void test()
    {
       list_of_ints lst;
       lst.push_back(1);
       lst.push_back(2);
       lst.push_back(3);
       int s = count_if(lst.begin(), lst.end(), [](int i){
          return i >= 2;
       });
       assert(s == 2);
       int d = distance(lst.begin(), lst.end());
       assert(d == 3);
    }
 

在接下来的章节中,我们将不再从头开始实现那么多自己的函数模板,而是开始逐步研究标准模板库提供的函数模板。但在我们结束对迭代器的深入讨论之前,还有一件事我想谈谈。

已弃用的 std::iterator

你可能会想:“我实现的每个迭代器类都需要提供相同的五个成员 typedef。这是大量的样板代码——如果我能的话,我很想将其简化。”有没有办法消除所有这些样板代码呢?

好吧,在 C++98 一直到 C++17 之前,标准库确实包含了一个帮助类模板来做到这一点。它的名字是 std::iterator,它接受五个模板类型参数,这五个参数对应于 std::iterator_traits 所需的五个成员 typedef。其中三个参数有“合理的默认值”,意味着最简单的用例得到了很好的覆盖:

    namespace std {
      template<
        class Category,
        class T,
        class Distance = std::ptrdiff_t,
        class Pointer = T*,
        class Reference = T&
      > struct iterator {
        using iterator_category = Category;
        using value_type = T;
        using difference_type = Distance;
        using pointer = Pointer;
        using reference = Reference;
      };
    }

    class list_of_ints_iterator :
      public std::iterator<std::forward_iterator_tag, int>
    {
       // ...
    };
 

不幸的是,对于 std::iterator 来说,现实生活并不那么简单;并且由于我们即将讨论的几个原因,std::iterator 在 C++17 中被弃用了。

正如我们在常量迭代器部分看到的那样,常量正确性要求我们为每个“非常量迭代器”类型提供一个 const 迭代器类型。因此,按照那个示例,我们真正得到的代码是这样的:

    template<
      bool Const,
      class Base = std::iterator<
        std::forward_iterator_tag,
        int,
        std::ptrdiff_t,
        std::conditional_t<Const, const int*, int*>,
        std::conditional_t<Const, const int&, int&>
      >
    >
    class list_of_ints_iterator : public Base
    {
      using typename Base::reference; // 这样做很笨拙!

      using node_pointer = std::conditional_t<Const, const list_node*,
        list_node*>;
      node_pointer ptr_;

    public:
      reference operator*() const { return ptr_->data; }
      // ...
    };
 

前面的代码并不比不使用 std::iterator 的版本更容易阅读或编写;而且,使用 std::iterator 的预期方式使我们的代码复杂化,涉及公共继承,也就是说,看起来很像典型的面向对象类层次结构。初学者可能会被诱惑,使用这个类层次结构来编写像这样的函数:

    template<typename... Ts, typename Predicate>
    int count_if(const std::iterator<Ts...>& begin,
                 const std::iterator<Ts...>& end,
                 Predicate pred);
 

一个函数通过接受基类引用类型的参数来实现不同的行为。但在 std::iterator 的情况下,这种相似性纯粹是偶然的,且具有误导性;从 std::iterator 继承并会给我们一个多态类层次结构,并且在我们自己的函数中引用那个“基类”从来都不是正确的做法!

因此,C++17 标准弃用了 std::iterator,目的是在 2020 年或更晚的某个标准中完全移除它。你不应该在你编写的代码中使用 std::iterator。

然而,如果你在你的代码库中使用 Boost,你可能会想看看 Boost 的 std::iterator 等价物,即 boost::iterator_facade。与 std::iterator 不同,boost::iterator_facade 基类为像 operator++(int) 和 operator!= 这样的棘手成员函数提供了默认功能,这些功能否则将是繁琐的样板代码。要使用 iterator_facade,只需从它继承并定义一些原始成员函数,如 dereference、increment 和 equal。(由于我们的列表迭代器是一个 ForwardIterator,这就是我们所需要的。对于 BidirectionalIterator,你还需要提供一个 decrement 成员函数,等等。)

由于这些原始成员函数是私有的,我们通过声明 friend class boost::iterator_core_access; 来授予 Boost 访问它们的权限。

    #include <boost/iterator/iterator_facade.hpp>

    template<bool Const>
    class list_of_ints_iterator : public boost::iterator_facade<
      list_of_ints_iterator<Const>,
      std::conditional_t<Const, const int, int>,
      std::forward_iterator_tag
    >
    {
      friend class boost::iterator_core_access;
      friend class list_of_ints;
      friend class list_of_ints_iterator<!Const>;

      using node_pointer = std::conditional_t<Const, const list_node*,
        list_node*>;
      node_pointer ptr_;

      explicit list_of_ints_iterator(node_pointer p) : ptr_(p) {} 

      auto& dereference() const { return ptr_->data; }
      void increment() { ptr_ = ptr_->next; }

      // Support comparison between iterator and const_iterator types
      template<bool R>
      bool equal(const list_of_ints_iterator<R>& rhs) const {
        return ptr_ == rhs.ptr_;}

    public:
      // Support implicit conversion of iterator to const_iterator
      // (but not vice versa)
      operator list_of_ints_iterator<true>() const { return
        list_of_ints_iterator<true>{ptr_}; }
    };
 

请注意,传递给 boost::iterator_facade 的第一个模板类型参数总是你正在编写定义的类:这是一个奇妙地重复出现的模板模式(Curiously Recurring Template Pattern)

使用 boost::iterator_facade 的列表迭代器代码比前一节中的相同代码明显更短;节省主要来自于不必重复关系运算符。因为我们的列表迭代器是一个 ForwardIterator,我们只有两个关系运算符;但如果它是一个 RandomAccessIterator,那么 iterator_facade 将基于单一的原始成员函数 distance_to 生成运算符 -、<、>、<= 和 >= 的默认实现。

总结

在本章中,我们了解到遍历是对数据结构可以进行的最基本操作之一。然而,仅仅使用原始指针来遍历复杂结构是不够的:对原始指针应用 ++ 往往不能以预期的方式“继续到下一个项目”。

C++ 标准模板库提供了迭代器的概念,作为原始指针的泛化。两个迭代器定义了数据的一个范围。这个范围可能只是容器内容的一部分;或者它可能根本不由任何内存支持,就像我们看到的 getc_iterator 和 putc_iterator 那样。迭代器类型的某些属性被编码在其迭代器类别中——输入、输出、前向、双向或随机访问——以便于函数模板可以在某些类别的迭代器上使用更快的算法。

如果你正在定义自己的容器类型,你也需要定义自己的迭代器类型——包括 const 和非 const 版本。模板是实现这一目的的便捷方式。在实现自己的迭代器类型时,避免使用已弃用的 std::iterator,但可以考虑使用 boost::iterator_facade。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值