2

I've created a Linked List in C++ and want to implement an iterator for it so that I can do range loops: for (const int& i : list) where Linked_List<int> list;.

My idea is to create the Iterator as part of the Linked_List class like this:

This is what I got so far:

template <typename T>
class Linked_List
{
public:
    struct Iterator;
    struct Node;
public:
    Linked_List();
    ~Linked_List() noexcept(false);
    Linked_List(const Linked_List&) = delete;
    Linked_List(Linked_List&&) = delete;
    Linked_List& operator=(const Linked_List&) = delete;
    Linked_List& operator=(Linked_List&&) = delete;

    void push_back(T);
    void push_front(T);
    void pop_back();
    void pop_front();
    bool empty() const;
    T back() const;
    T front() const;
    //void swap(T, T);
    //void insert(Iterator, T);
    //void erase(Iterator);
    //Iterator begin() const;
    //Iterator end() const;
private:
    Node* head;
    Node* tail;
};

template<typename T>
struct Linked_List<T>::Node
{
    Node() : prev(nullptr), next(nullptr) {}
    Node(T t) : value(t), prev(nullptr), next(nullptr) {}
    Node* prev;
    Node* next;
    T value;
};
  1. Is this a good approach?
  2. Should I do error checking when incrementing the list to check if current->next == tail? If so, how do I do that? Because my Iterator doesn't have a list object with a tail.

Edit: I'm not sure how to implement the struct Iterator;, I get stuck when figuring out how to connect it with the list so that I can check if the current node returned from the iterator equals the tail in the list, in the Linked_List Iterator end() const method.

Let's say I've implemented all the necessary operators for an iterator like this:

struct Iterator
{
    T& operator*() const { return current->value; }
    bool operator!=(const Iterator& rhs) { return (*_current != rhs._current); }
    Iterator& operator++()
    {
        current = current->next;
        return *this;
    }
};

How would I go about implementing Iterator Linked_List<T>::begin() const; and end() now?

I imagine an imaginary user making an iterator object like this: Linked_List<int>::Iterator it;

An idea is to have a public constructor with no parameters and a private constructor that takes a node as a parameter which _current will be set to, and have the Linked_List class as a friend.

user644361
  • 272
  • 1
  • 5
  • 15
  • 1
    1/ it's fine. If you end up with multiple containers with similar iterator implementations, it might be worth factoring that out then. 2/ you don't need to (it's a user error, not a normal runtume condition). But if you want to, (maybe in DEBUG builds) then consider what the `next` pointer of the last element should look like. Actually, consider how you're going to implement one-past-the-end iterator without a sentinel node anyway. – Useless Mar 28 '19 at 16:48
  • This might be heading in the direction of overkill for your needs, but [Writing your own STL Container](https://stackoverflow.com/questions/7758580/writing-your-own-stl-container) should give you an idea of some of the things you may want to take into account. – user4581301 Mar 28 '19 at 16:50
  • 1
    Does it currently work? If yes, you'd better post the full code to CodeReview. People there are very good at fully reviewing your code to find possible improvements – Serge Ballesta Mar 28 '19 at 16:51
  • Sidenote: You will almost certainly need a destructor for clean-up, and if that's the case, [the Rules of Three and Five](https://en.cppreference.com/w/cpp/language/rule_of_three) will come into affect. – user4581301 Mar 28 '19 at 16:52
  • Another note: Since you have iterators, or plan to, I recommend making the `Node` `private`. A user can cause a lot of damage to the linked list if they get their grubby little hands on a `Node` – user4581301 Mar 28 '19 at 16:54
  • @user4581301 Thanks for the link. Yes it needs a destructor. The Iterator class also needs to access `Node`, that's why it's public atm. – user644361 Mar 28 '19 at 17:37
  • @SergeBallesta no, I haven't implemented it yet. Because I'm not sure how to make the Iterator contain the current list without having to pass it in the constructor of the iterator. I need to be able to check if the iterator is not equal to the tail in the `Iterator end() const` method. – user644361 Mar 28 '19 at 17:39
  • But... why? why roll your own version of `std::list`? – Tzalumen Mar 28 '19 at 18:41
  • 1
    @Tzalumen to learn – user644361 Mar 28 '19 at 18:44
  • Sooner or later you have to learn to manage your own resources, and nothing beats linked list for teaching the soul-crushing truth that you want to avoid it. – user4581301 Mar 28 '19 at 19:26
  • moved: https://codereview.stackexchange.com/questions/216444/c-linked-list-iterator-implementation – user644361 Mar 28 '19 at 20:13

1 Answers1

4

A few notes.

There are two options where to declare Node and Iterator. Inside the list class as List<T>::Node or outside as Node<T>. It is, in part, a matter of taste. From engineering perspective though, the symbol names are longer for nested classes, so your debuginfo is bigger. Also, when nested classes are also templates it is harder to specialize them if/when necessary (because that requires fully specializing the enclosing template first), but this is not the case here.

It leads to more elegant code when one list node is used as list head and tail. Empty list is a node whose next and prev point to itself. push_front appends to list.next which points to the first node or itself. push_back appends a node to list.prev which points to the last node or itself. When inserting/removing nodes there is no need to have special handling of the first and last nodes. E.g. :

struct Node {
    Node *next_, *prev_;

    Node() 
        : next_(this), prev_(this)
    {}

    ~Node() { 
        unlink();
    }

    void push_back(Node* n) {
        n->next_ = this;
        n->prev_ = prev_;
        prev_->next_ = n;
        prev_ = n;
    }

    void unlink() {
        Node *next = next_, *prev = prev_;
        next->prev_ = prev;
        prev->next_ = next;
        next_ = this;
        prev_ = this;
    }
};

In the above, Node only needs two operations to be able to maintain a list. More than that, Node is itself a minimalist list that can be used for intrusive lists (with auto-unlink in the destructor). Note how using this makes checks for nullptr unnecessary - Node is always a valid list.

Error checking should be in debug mode only (use assert, for example). Otherwise, those checks penalise correct applications with unnecessary run-time checks.


Here is a minimal working example based on the ideas for you:

template<class T>
class List;

class Iterator;

class Node {
    friend class Iterator;
    template<class T> friend class List;

protected:
    Node *next_, *prev_;

    void push_back(Node* n) {
        n->next_ = this;
        n->prev_ = prev_;
        prev_->next_ = n;
        prev_ = n;
    }

    void unlink() {
        Node *next = next_, *prev = prev_;
        next->prev_ = prev;
        prev->next_ = next;
        next_ = this;
        prev_ = this;
    }

public:
    Node()
        : next_(this), prev_(this)
    {}

    ~Node() { unlink(); }
};

class Iterator {
protected:
    Node* node_;

    Iterator(Node* node)
        : node_(node)
    {}

public:
    Iterator& operator++() {
        node_ = node_->next_;
        return *this;
    }

    bool operator==(Iterator b) const { return node_ == b.node_; }
    bool operator!=(Iterator b) const { return node_ != b.node_; }

    // Implement the rest of iterator interface.
};

template<class T>
class List {
    class NodeT : public Node {
        friend class List<T>;
        T value_;
        NodeT(T t) : value_(t) {}
    };

    template<class U>
    class IteratorT : public Iterator {
        friend class List<T>;
        NodeT* node() const { return static_cast<NodeT*>(node_); }
    public:
        U& operator*() const { return node()->value_; }
        U* operator->() const { return &node()->value_; }
        operator IteratorT<U const>() const { return node_; } // iterator to const_iterator conversion
        IteratorT(Node* node) : Iterator{node} {}
    };

    Node list_;

public:
    using iterator = IteratorT<T>;
    using const_iterator = IteratorT<T const>;

    ~List() { clear(); }

    bool empty() const { return list_.next_ == &list_; }

    iterator begin() { return list_.next_; }
    iterator end() { return &list_; }

    void push_back(T t) { list_.push_back(new NodeT(t)); }
    void erase(const_iterator i) { delete i.node(); }

    void clear() {
        while(!empty())
            erase(begin());
    }

    // Implement the rest of the functionality.
};

int main() {
    List<int> l;
    l.push_back(1);
    l.push_back(2);
    l.push_back(3);
    for(auto elem : l)
        std::cout << elem << ' ';
    std::cout << '\n';
}
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • "It leads to more elegant code when one list node is used as list head and tail": Do you mean like in a circular list where `node` is the `head` and `node->prev` is the `tail`? Could you also elaborate on a good way to implement to iterator? the iterator is really my main concern. Especially the functions that use the iterator in the `Linked_List` class. Thanks for the tip on how `this` makes checking `nullptr` unnecessary, didnt' know that. Why do they penalize correct applications when correct applications don't throw the error? – user644361 Mar 28 '19 at 20:51
  • what do I do if I need the to return the `node_` from the iterator class, in case I want to remove it from `List`? or `erase(Iterator)` it? Because I've already implemented `operator*()` to return the `value_` of `node_` – user644361 Mar 28 '19 at 22:08
  • 2
    the `unlink()` in node is genius. But should a node have a `push_back()`? Really thank you for everything. – user644361 Mar 29 '19 at 12:03
  • 1
    @user644361 `unlink` and `push_back` are two low-level fundamental operations that all other operations build upon. – Maxim Egorushkin Mar 29 '19 at 12:33
  • 2
    @user644361 Please note that I didn't encapsulate it with `public` and `private`. `Node::unlink` and `Node::push_back` should only be accessible by `List` class. The user should not be calling those directly. You may like to add `private`/`public` as necessary, I only focused on getting the functionality right. Also, `Iterator::node` should not be callable by the user. – Maxim Egorushkin Mar 29 '19 at 16:34
  • 2
    @user644361 Yep, all private, unless it has to be public. Make `List` a friend of `NodeT` and `IteratorT`. – Maxim Egorushkin Mar 29 '19 at 16:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/190924/discussion-between-user644361-and-maxim-egorushkin). – user644361 Mar 29 '19 at 16:39
  • It is clear to me why you have `push_back()` inside the node class now, it's genious. – user644361 Mar 30 '19 at 12:45
  • but I have some questions about your code. Why does erase work, isn't the iterator invalidated after the first erase? Why isn't `list_` a pointer? – user644361 Mar 30 '19 at 12:51
  • @user644361 In lists only the iterator to the erased element is invalidated. Not sure I understand your question. – Maxim Egorushkin Mar 30 '19 at 14:19
  • with `std::list` you have to do `it = erase(it)`, but you use `void`. How does the iterator stay valid? – user644361 Mar 30 '19 at 14:20
  • @user644361 You can easily make `erase` return the iterator to the next element, like `std::list::erase` does. – Maxim Egorushkin Mar 30 '19 at 14:22
  • @user644361 You may like to step through `push_back` and `erase` and think about what exactly happens. – Maxim Egorushkin Mar 30 '19 at 14:24
  • Let's say you do `auto it = begin()` and now `list.erase(it)`, now `it` is invalidated. You can no longer use that `it`. Nvm, I think I understand. – user644361 Mar 30 '19 at 14:25
  • 1
    @user644361 That is true. It is also true for `std::list`. But you can do `list.erase(it++)` and `it` is a valid iterator to the next element. This is how `std::list::erase` returns you that next iterator. – Maxim Egorushkin Mar 30 '19 at 14:27
  • `iterator begin() { return list_.next_; }` and `iterator end() { return &list_; }` Are these 2 correct? I dont understand these two. – Sourav Ganguly Mar 07 '22 at 17:38
  • @SouravGanguly The code is correct. It is explained in the answer: _It leads to more elegant code when **one list node is used as list head and tail**. Empty list is a node whose next and prev point to itself._ – Maxim Egorushkin Mar 07 '22 at 22:05
  • 1
    Ok got it, thanks. Reading properly instead of skim reading helps :) – Sourav Ganguly Mar 08 '22 at 15:42
  • Hey, I would love your comments on this :) - https://codereview.stackexchange.com/questions/274890/forward-linked-list-implementation-in-c – Sourav Ganguly Mar 13 '22 at 19:53