淺談如何實現自定義的 iterator 之二

hedzr發表於2021-11-04

實現你自己的迭代器 II

實現一個樹結構容器,然後為其實現 STL 風格的迭代器例項。

本文是為了給上一篇文章 淺談如何實現自定義的 iterator 提供補充案例。

tree_t 的實現

我打算實現一個簡單而又不簡單的樹容器,讓它成為標準的檔案目錄結構式的容器型別。但簡單就在於,我只準備實現最最必要的幾個樹結構的介面,諸如遍歷啦什麼的。

這是一個很標準的檔案目錄的模擬品,致力於完全仿照資料夾的表現。它和什麼 binary tree,AVL,又或是紅黑樹什麼的完全是風馬牛不相及。

首先可以確定的是 tree_t 依賴於 generic_node_t,tree_t 自身並不真的負責樹的演算法,它只是持有一個 root node 指標。所有與樹操作相關的內容都在 generic_node_t 中。

tree_t

因此下面首先給出 tree_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

其中的必要的介面基本上都轉向到 _root 中了。

generic_node_t

再來研究 node 的實現。

一個樹節點持有如下的資料:

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{};
    
    // ...
  }
}

據此我們可以實現節點的插入、刪除以及基本的訪問操作。

這些內容因為篇幅原因就略去了。

如果你感興趣的話,請查閱原始碼 dp-tree.hhtree.cc

正向迭代器

下面給出它的正向迭代器的完整實現,以便對上一篇文章做出更完整的交代。

正向迭代器是指 begin() 和 end() 及其代表的若干操作。簡單來說,它支援從開始到結束的單向的容器元素遍歷。

對於樹結構來說,begin() 是指根節點。遍歷演算法是根 - 左子樹 - 右子樹,也就是前序遍歷演算法。這和 AVL 等主要使用中序遍歷有著完全不同的思路。

據此,end() 指的是 right of 最右最低的子樹的最右最低葉子節點。什麼意思?在最後一個葉子節點向後再遞增一次,實質上是將 _invalid 標誌置為 true 來表示已經抵達終點。

為了避免 STL end() 迭代器求值會發生訪問異常的情況,我們實現的 end() 是可以安全求值的,儘管求值結果實際上沒有意義(end() - 1 才是正確的 back() 元素)。
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); }

    // ...
  }
}

這個正向迭代器從根節點開始從上至下、從左至右對樹結構進行遍歷。

有句話怎麼說的來著,高手隨隨便便一站著全身都是破綻然後就全數都冇破綻了。對於 preorder_iter_data 來說也有點這個味道:細節太多之後,讓他們全都圓滿之後,然後就無法評講程式碼實現的理由了。

只是講笑,實際上是講述起來太耗費篇幅,所以你直接看程式碼,我就省筆墨。

反向迭代器

類似於正向迭代器,但是具體演算法不同。

本文中限於篇幅不予列出,如果你感興趣的話,請查閱原始碼 dp-tree.hhtree.cc

需要照顧到的事情

再次複述完全手寫迭代器的注意事項,並且補充一些上回文中沒有精細解說的內容,包括:

  1. begin() 和 end()
  2. 迭代器嵌入類(不必被限定為嵌入),至少實現:

    1. 遞增運算子過載,以便行走
    2. 遞減運算子過載,如果是雙向行走(bidirectional_iterator_tag)或隨機行走(random_access_iterator_tag)
    3. operator* 運算子過載,以便迭代器求值:使能 (*it).xxx
    4. 配套實現 operator-> ,以使能 it->xxx
    5. operator!= 運算子過載,以便計算迭代範圍;必要時也可以顯式過載 operator==(預設時編譯器自動從 != 運算子上生成一個配套替代品)

補充說明:

  1. 為了能與 STL 的 <algorithm> 演算法相容,你需要手動定義 iterator traits,如同這樣:

    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;
    }

    這麼做的目的在於讓 std::find_if 等等 algorithms 能夠透過你宣告的 iterator_catagory 而正確引用 distance、advance、++ or -- 等等實現。如果你的 iterator 不支援雙向行走,那麼 -- 會被模擬:從容器的第一個元素開始遍歷並登記,直到行走到 it 所在的位置,然後將 last_it 返回。其它的多數謂詞也都會有類似的模擬版本。

原本,這些 traits 是通過從 std::iterator 派生而自動被定義的。但是自 C++17 起,暫時建議直接手工編寫和定義它們。

你可以不必定義它們,這並不是強制。

  1. 絕大多數情況下,你宣告 std::forward_iterator_tag 型別,並定義 ++ 運算子與其配套;如果你定義為 std::bidirectional_iterator_tag 型別,那麼還需要定義 -- 運算子。

    自增自減運算子需要同時定義字首與字尾,請參考上一篇文章 淺談如何實現自定義的 iterator 中的有關章節。

  2. 在迭代器中,定義 begin() 與 end(),以便在容器類中借用它們(在本文的 tree_t 示例中,容器類指的是 generic_node_t。
  3. 如果你想要定義 rbegin/rend,它們並不是 -- 的替代品,它們通常需要你完全獨立於正向迭代器而單獨定義另外一套。在 tree_t 中對此有明確的實現,但本文中限於篇幅不予列出,如果你感興趣的話,請查閱原始碼 dp-tree.hhtree.cc

使用/測試程式碼

一些測試用的程式碼列舉一下:

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';
}

這些程式碼只是單純地展示了用法,並沒有按照單元測試的做法來書寫——也無此必要。

後記

本文給出了一個真實工作的容器類已經相應的迭代器實現,我相信它們將是你的絕佳的編碼實現範本。

相關文章