How to implement the customized iterator

Keywords: Algorithm STL iterator

Implement your own iterator II

Implement a tree structure container, and then implement an STL style iterator instance for it.

This article is for the last article How to implement user defined iterator Provide supplementary cases.

tree_ Implementation of T

I intend to implement a simple but not simple tree container and make it a standard container type with file directory structure. But the simplicity is that I'm only going to implement the most necessary tree structure interfaces, such as traversal.

This is a very standard copy of the file directory, which is committed to completely imitating the performance of the folder. It is completely different from binary tree, AVL, or red and black tree.

The first thing you can determine is the tree_t depends on generic_node_t,tree_t itself is not really responsible for the tree algorithm, it just holds a root node pointer. All content related to tree operation is in generic_node_t medium.

tree_t

Therefore, we first give the tree_ Specific implementation of T:

namespace dp::tree{
  template<typename Data, typename Node = detail::generic_node_t<Data>>
  class tree_t : detail::generic_tree_ops<Node> {
    public:
    using Self = tree_t<Data, Node>;
    using BaseT = detail::generic_tree_ops<Node>;
    using NodeT = Node;
    using NodePtr = Node *;
    using iterator = typename Node::iterator;
    using const_iterator = typename Node::const_iterator;
    using reverse_iterator = typename Node::reverse_iterator;
    using const_reverse_iterator = typename Node::const_reverse_iterator;

    using difference_type = std::ptrdiff_t;
    using value_type = typename iterator::value_type;
    using pointer = typename iterator::pointer;
    using reference = typename iterator::reference;
    using const_pointer = typename iterator::const_pointer;
    using const_reference = typename iterator::const_reference;

    ~tree_t() { clear(); }

    void clear() override {
      if (_root) delete _root;
      BaseT::clear();
    }

    void insert(Data const &data) {
      if (!_root) {
        _root = new NodeT{data};
        return;
      }
      _root->insert(data);
    }
    void insert(Data &&data) {
      if (!_root) {
        _root = new NodeT{data};
        return;
      }
      _root->insert(std::move(data));
    }
    template<typename... Args>
    void emplace(Args &&...args) {
      if (!_root) {
        _root = new NodeT{std::forward<Args>(args)...};
        return;
      }
      _root->emplace(std::forward<Args>(args)...);
    }

    Node const &root() const { return *_root; }
    Node &root() { return *_root; }

    iterator begin() { return _root->begin(); }
    iterator end() { return _root->end(); }
    const_iterator begin() const { return _root->begin(); }
    const_iterator end() const { return _root->end(); }
    reverse_iterator rbegin() { return _root->rbegin(); }
    reverse_iterator rend() { return _root->rend(); }
    const_reverse_iterator rbegin() const { return _root->rbegin(); }
    const_reverse_iterator rend() const { return _root->rend(); }

    private:
    NodePtr _root{nullptr};
  }; // class tree_t

} // namespace dp::tree

The necessary interfaces are basically turned to_ root.

generic_node_t

Let's study the implementation of node.

A tree node holds the following data:

namespace dp::tree::detail{
  template<typename Data>
  struct generic_node_t {
    using Node = generic_node_t<Data>;
    using NodePtr = Node *; //std::unique_ptr<Node>;
    using Nodes = std::vector<NodePtr>;

    private:
    Data _data{};
    NodePtr _parent{nullptr};
    Nodes _children{};
    
    // ...
  }
}

Based on this, we can realize the insertion, deletion and basic access operations of nodes.

These contents have been omitted for space reasons.

If you are interested, please refer to the source code dp-tree.hh and tree.cc.

Forward iterator

The complete implementation of its forward iterator is given below in order to make a more complete explanation of the previous article.

Forward iterators refer to begin() and end() and the operations they represent. Simply put, it supports one-way traversal of container elements from start to end.

For a tree structure, begin() refers to the root node. Traversal algorithm is root left subtree right subtree, that is, preorder traversal algorithm. This is a completely different idea from AVL, which mainly uses medium order traversal.

Accordingly, end() refers to the rightmost and lowest leaf node of the rightmost and lowest subtree. what do you mean? Incrementing the last leaf node backward again is essentially_ The invalid flag is set to true to indicate that the destination has been reached.

In order to avoid access exceptions in the evaluation of STL end() iterator, the end() we implemented can be evaluated safely, although the evaluation result is actually meaningless (end() - 1 is the correct back() element).

namespace dp::tree::detail{
  template<typename Data>
  struct generic_node_t {

    // ...

    struct preorder_iter_data {

      // iterator traits
      using difference_type = std::ptrdiff_t;
      using value_type = Node;
      using pointer = value_type *;
      using reference = value_type &;
      using iterator_category = std::forward_iterator_tag;
      using self = preorder_iter_data;
      using const_pointer = value_type const *;
      using const_reference = value_type const &;

      preorder_iter_data() {}
      preorder_iter_data(pointer ptr_, bool invalid_ = false)
        : _ptr(ptr_)
          , _invalid(invalid_) {}
      preorder_iter_data(const preorder_iter_data &o)
        : _ptr(o._ptr)
          , _invalid(o._invalid) {}
      preorder_iter_data &operator=(const preorder_iter_data &o) {
        _ptr = o._ptr, _invalid = o._invalid;
        return *this;
      }

      bool operator==(self const &r) const { return _ptr == r._ptr && _invalid == r._invalid; }
      bool operator!=(self const &r) const { return _ptr != r._ptr || _invalid != r._invalid; }
      reference data() { return *_ptr; }
      const_reference data() const { return *_ptr; }
      reference operator*() { return data(); }
      const_reference operator*() const { return data(); }
      pointer operator->() { return &(data()); }
      const_pointer operator->() const { return &(data()); }
      self &operator++() { return _incr(); }
      self operator++(int) {
        self copy{_ptr, _invalid};
        ++(*this);
        return copy;
      }

      static self begin(const_pointer root_) {
        return self{const_cast<pointer>(root_)};
      }
      static self end(const_pointer root_) {
        if (root_ == nullptr) return self{const_cast<pointer>(root_)};
        pointer p = const_cast<pointer>(root_), last{nullptr};
        while (p) {
          last = p;
          if (p->empty())
            break;
          p = &((*p)[p->size() - 1]);
        }
        auto it = self{last, true};
        ++it;
        return it;
      }

      private:
      self &_incr() {
        if (_invalid) {
          return (*this);
        }

        auto *cc = _ptr;
        if (cc->empty()) {
          Node *pp = cc;
          size_type idx;
          go_up_level:
          pp = pp->parent();
          idx = 0;
          for (auto *vv : pp->_children) {
            ++idx;
            if (vv == _ptr) break;
          }
          if (idx < pp->size()) {
            _ptr = &((*pp)[idx]);
          } else {
            if (pp->parent()) {
              goto go_up_level;
            }
            _invalid = true;
          }
        } else {
          _ptr = &((*cc)[0]);
        }
        return (*this);
      }

      pointer _ptr{};
      bool _invalid{};
      // size_type _child_idx{};
    };

    using iterator = preorder_iter_data;
    using const_iterator = iterator;
    iterator begin() { return iterator::begin(this); }
    const_iterator begin() const { return const_iterator::begin(this); }
    iterator end() { return iterator::end(this); }
    const_iterator end() const { return const_iterator::end(this); }

    // ...
  }
}

This forward iterator traverses the tree structure from top to bottom and left to right from the root node.

There's a saying. The master stands casually, his whole body is full of flaws, and then there are no flaws. For preorder_iter_data is also a bit like this: after too many details, let them all be perfect, and then you can't comment on the reasons for code implementation.

Just talking about laughter actually takes too much space, so if you look at the code directly, I'll save pen and ink.

reverse iterator

It is similar to the forward iterator, but the specific algorithm is different.

This article is limited to space and will not be listed. If you are interested, please refer to the source code dp-tree.hh and tree.cc.

Things to take care of

Repeat the notes of the completely handwritten iterator again, and add some contents that are not explained in detail in the previous palindrome, including:

  1. begin() and end()
  2. Iterator embedding class (not necessarily limited to embedding), which at least implements:

    1. The increment operator is overloaded for walking
    2. Decrement operator overload, if it is bidirectional_iterator_tag or random_access_iterator_tag
    3. Operator * operator overload for iterator evaluation: enable (* it).xxx
    4. Implement operator - > to enable it - > XXX
    5. operator!= Operator overloading to calculate the iteration range; You can also explicitly overload operator = = (by default, the compiler automatically generates a matching substitute from the! = operator)

Supplementary notes:

  1. In order to be compatible with STL's < algorithm > algorithm, you need to manually define iterator traits, as follows:

    struct preorder_iter_data {
    
      // iterator traits
      using difference_type = std::ptrdiff_t;
      using value_type = Node;
      using pointer = value_type *;
      using reference = value_type &;
      using iterator_category = std::forward_iterator_tag;
    }

    The purpose of this is to make std::find_if and so on, algorithms can be announced through your iterator_ Category and correctly reference the implementation of distance, advance, + + or -- and so on. If your iterator does not support two-way walking, then -- will be simulated: traverse and register from the first element of the container until you walk to the location of it, and then set last_it returns. Most other predicates will have similar mock versions.

Originally, these traits were automatically defined by deriving from std::iterator. However, since C++17, it is temporarily recommended to write and define them directly by hand.

You don't have to define them. It's not mandatory.

  1. In most cases, you declare std::forward_iterator_tag type, and define + + operators to match it; If you define std::bidirectional_iterator_tag type, you also need to define the -- operator.

    Self increasing and self decreasing operators need to define prefix and suffix at the same time. Please refer to the previous article How to implement user defined iterator Relevant chapters in.

  2. In the iterator, define begin() and end() to borrow them from the container class (in the tree_t example in this article, the container class refers to generic_node_t).
  3. If you want to define rbegin/rend, they are not substitutes for - - they usually require you to define another set completely independent of the forward iterator. There is a clear implementation of this in tree_t, but it is not listed in this article due to space limitation. If you are interested, please refer to the source code dp-tree.hh and tree.cc.

Use / test code

Some test codes are listed below:

void test_g_tree() {
  dp::tree::tree_t<tree_data> t;
  UNUSED(t);
  assert(t.rbegin() == t.rend());
  assert(t.begin() == t.end());

  std::array<char, 128> buf;

  //     1
  // 2 3 4 5 6 7
  for (auto v : {1, 2, 3, 4, 5, 6, 7}) {
    std::sprintf(buf.data(), "str#%d", v);
    // t.insert(tree_data{v, buf.data()});
    tree_data vd{v, buf.data()};
    t.insert(std::move(vd));
    // tree_info(t);
  }

  {
    auto v = 8;
    std::sprintf(buf.data(), "str#%d", v);
    tree_data td{v, buf.data()};
    t.insert(td);

    v = 9;
    std::sprintf(buf.data(), "str#%d", v);
    t.emplace(v, buf.data());

    {
      auto b = t.root().begin(), e = t.root().end();
      auto &bNode = (*b), &eNode = (*e);
      std::cout << "::: " << (*bNode) << '\n'; // print bNode.data()
      std::cout << "::: " << (eNode.data()) << '\n';
    }

    {
      int i;
      i = 0;
      for (auto &vv : t) {
        std::cout << i << ": " << (*vv) << ", " << '\n';
        if (i == 8) {
          std::cout << ' ';
        }
        i++;
      }
      std::cout << '\n';
    }

    using T = decltype(t);
    auto it = std::find_if(t.root().begin(), t.root().end(), [](typename T::NodeT &n) -> bool { return (*n) == 9; });

    v = 10;
    std::sprintf(buf.data(), "str#%d", v);
    it->emplace(v, buf.data());

    v = 11;
    std::sprintf(buf.data(), "str#%d", v);
    (*it).emplace(v, buf.data());

    #if defined(_DEBUG)
    auto const itv = t.find([](T::const_reference n) { return (*n) == 10; });
    assert(*(*itv) == 10);
    #endif
  }

  //

  int i;

  i = 0;
  for (auto &v : t) {
    std::cout << i << ": " << (*v) << ", " << '\n';
    if (i == 8) {
      std::cout << ' ';
    }
    i++;
  }
  std::cout << '\n';

  i = 0;
  for (auto it = t.rbegin(); it != t.rend(); ++it, ++i) {
    auto &v = (*it);
    std::cout << i << ": " << (*v) << ", " << '\n';
    if (i == 8) {
      std::cout << ' ';
    }
  }
  std::cout << '\n';
}

These codes simply show usage and are not written in accordance with the practice of unit testing -- nor is it necessary.

Postscript

This article gives a real working container class, which has been implemented by the corresponding iterator. I believe they will be your excellent coding implementation template.

Posted by crochk on Sun, 31 Oct 2021 05:05:17 -0700