Commented source code of what it would mean to implement the core of a linked list in C++.

Introduction

I have chosen to implement a circular double linked list with the dummy node in the header:

Double linked dummy node in header

The dl_list class below is a template on the sequence type for:

  • a double linked list
  • circular
  • with dummy node in the header
  • non-intrusive (nodes are provided by the list class)
  • nodes are allocated on the heap (no custom allocator) and list owns the nodes
  • getting size is linear time with number of elements (but splice is constant time)

The short name is misleading. To be explicit about all these variations would have made for a long name.

Another approach could have been a policy-based design. E.g. decisions such as caching the size or not would have been a choice made via an additional template parameter. But then still we would have to specify a lot of template parameters, as there are many choices to be made, leading again to a long actual name in practice.

dl_list.h file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
#pragma once

#include <cstddef> // for size_t and ptrdiff_t
#include <iterator> // for iterator tag and std::reverse_iterator
#include <utility> // for std::move and std::forward

template<typename T>
class dl_list {

  // Nodes consist of links and value
  struct node;

  struct links {
    node * next_;
    node * prev_;
  };

  struct node : public links {
    T value_;

    // Constructor: next, prev, then args for value construction
    template<typename ... Args>
    node(node * next, node * prev, Args && ... args) :
      links{ next, prev }, value_(std::forward<Args>(args)...)
    {}
  };

  // Dummy node only has links.
  // Design decision: the dummy node also deals with ownership, and move to
  // help implement the list.
  struct dummy_node : public links {

    // Helper to look at this as a node
    node * as_node_ptr() {
      return reinterpret_cast<node*>(this);
    }

    // or as a const node
    const node * as_node_ptr() const {
      return reinterpret_cast<const node*>(this);
    }

    // Default constructor, next_ and prev_ point at this.
    dummy_node() noexcept : links{ as_node_ptr(), as_node_ptr() }  {}

    // Free nodes in the destructor
    ~dummy_node() {
      free();
    }

    // No copy
    dummy_node(const dummy_node &) = delete;
    dummy_node & operator= (const dummy_node &) = delete;

    // Implement move constructor
    dummy_node(dummy_node && other) noexcept {
      move_impl(other.as_node_ptr());
    }

    // and move assignment.
    // It turns out they both use a helper function move_impl
    dummy_node & operator= (dummy_node && other) noexcept {
      if (this != &other) {
        free();
        move_impl(other.as_node_ptr());
      }
      return *this;
    }

  // Dummy node helper functions
  private:
    // To free nodes, walk the chain following prev_ and delete the nodes,
    // except this (the dummy_node for this list is part of the header)
    void free() noexcept {
      node * crt = this->prev_;
      while(crt != as_node_ptr()) {
        node * tmp = crt;
        crt = crt->prev_;
        delete tmp;
      }
    }

    // Move helper function. This takes the minimalistic view that the moved
    // from object (other) is in a state that it can be destroyed or assigned
    // to. That state is prev_ pointing to &other (which is also what free
    // tests for ownership).
    void move_impl(node * other) noexcept {
        if (other->prev_ == other) {
          this->next_ = as_node_ptr();
          this->prev_ = as_node_ptr();
        }
        else {
          this->next_ = other->next_;
          this->prev_ = other->prev_;
          this->next_->prev_ = as_node_ptr();
          this->prev_->next_ = as_node_ptr();
          other->prev_ = other;
        }
    }

  };

  // A dummy_node is all that the header is
  dummy_node header_;

public:

  // Iterator
  class iterator {
  public:
    // Type aliases used by algorithms (e.g. iterator_category is used for
    // algorithm selection)
    using iterator_category = std::bidirectional_iterator_tag;
    using difference_type = std::ptrdiff_t;
    using value_type = T;
    using pointer = T *;
    using reference = T &;

  private:
    // has a pointer to a node
    node * ptr_;

    // It has a constructor that the list uses to initialize the pointer to the
    // node
    explicit iterator(node * ptr) : ptr_{ ptr } {};
    // friendship required for the list to access the above constructor
    friend class dl_list;

  public:
    // Default constructor. Needs to initialize pointer to nullptr to ensure
    // equality comparison with other iterator object gives consistent result
    iterator() : ptr_{ nullptr } {};

    // Copy and move are default implemented by the compiler

    // Equality compares node pointer for equality
    friend bool operator== (const iterator & x, const iterator & y) {
      return x.ptr_ == y.ptr_;
    }

    // Difference is negation of equality
    friend bool operator!= (const iterator & x, const iterator & y) {
      return !(x == y);
    }

    // Dereferencing returns the value type
    T & operator* () {
      return ptr_->value_;
    }

    // Increment and decrement, pre and post
    iterator & operator++ () {
      ptr_ = ptr_->next_;
      return *this;
    }

    iterator operator++ (int) {
      node * tmp = ptr_;
      ptr_ = ptr_->next_;
      return iterator(tmp);
    }

    iterator & operator-- () {
      ptr_ = ptr_->prev_;
      return *this;
    }

    iterator operator-- (int) {
      node * tmp = ptr_;
      ptr_ = ptr_->prev_;
      return iterator(tmp);
    }
  };

  // Const iterator
  class const_iterator {
  public:
    // NOTE: The value_type does not have a const. Reason is one can use it
    // like:
    //   std::iterator_traits<I>::value_type x = *it;
    using iterator_category = std::bidirectional_iterator_tag;
    using difference_type = std::ptrdiff_t;
    using value_type = T;
    using pointer = const T *;
    using reference = const T &;

  private:
    // has a pointer to a const node
    const node * ptr_;

    // Same private constructor and friend
    explicit const_iterator(const node * ptr) : ptr_{ ptr } {};
    friend class dl_list;

  public:
    // Default constructor.
    const_iterator() : ptr_{ nullptr } {};

    // Copy and move are default implemented by the compiler

    // Construct from an iterator (not explicit)
    const_iterator(const iterator & x) noexcept : ptr_{ x.ptr_ } {}

    // Equality and difference
    friend bool operator== (const const_iterator & x, const const_iterator & y) {
      return x.ptr_ == y.ptr_;
    }

    friend bool operator!= (const const_iterator & x, const const_iterator & y) {
      return !(x == y);
    }

    // Dereferencing returns the value type
    const T & operator* () {
      return ptr_->value_;
    }

    // Increment and decrement, pre and post
    const_iterator & operator++ () {
      ptr_ = ptr_->next_;
      return *this;
    }

    const_iterator operator++ (int) {
      node * tmp = ptr_;
      ptr_ = ptr_->next_;
      return const_iterator(tmp);
    }

    const_iterator & operator-- () {
      ptr_ = ptr_->prev_;
      return *this;
    }

    const_iterator operator-- (int) {
      node * tmp = ptr_;
      ptr_ = ptr_->prev_;
      return const_iterator(tmp);
    }
  };

  // Type alises
  using value_type = T;
  using reference = T &;
  using const_reference = const T &;
  // using iterator alias not required, see struct iterator above
  // same for const_iterator
  using reverse_iterator = std::reverse_iterator<iterator>;
  using const_reverse_iterator = std::reverse_iterator<const_iterator>;
  using difference_type = std::ptrdiff_t;
  using size_type = std::size_t;

  // Default constructor, nothing to do the dummy_node default constructor does
  // the job
  dl_list() noexcept {
  }

  // Initialize from a sequence. Shoud push_back throw, the dummy_node
  // destructor will free nodes already allocated
  template<typename I, typename S>
  // requires I is InputIterator and S is sentinel for It
  dl_list(I first, S last) {
    while (first != last) {
      push_back(*first);
      ++first;
    }
  }

  // Copy constructor falls back on initializing from sequence
  dl_list(const dl_list & other) : dl_list(other.begin(), other.end()) {
  }

  // Copy assignment makes a copy and moves header
  dl_list & operator= (const dl_list & other) {
    dl_list tmp(other);
    header_ = std::move(tmp.header_);
    return *this;
  }

  // Move constructor and assignment delegate to header
  // defaults do the job
  dl_list(dl_list &&) noexcept = default;
  dl_list & operator= (dl_list &&) noexcept = default;

  // Begin, end, const and reverse variants
  iterator begin() noexcept {
    return iterator(sentinel()->next_);
  }

  const_iterator begin() const noexcept {
    return const_iterator(sentinel()->next_);
  }

  const_iterator cbegin() const noexcept {
    return const_iterator(sentinel()->next_);
  }

  iterator end() noexcept {
    return iterator(sentinel());
  }

  const_iterator end() const noexcept {
    return const_iterator(sentinel());
  }

  const_iterator cend() const noexcept {
    return const_iterator(sentinel());
  }

  reverse_iterator rbegin() noexcept {
    return reverse_iterator(end());
  }

  const_reverse_iterator rbegin() const noexcept {
    return reverse_iterator(end());
  }

  const_reverse_iterator crbegin() const noexcept {
    return reverse_iterator(cend());
  }

  reverse_iterator rend() noexcept {
    return reverse_iterator(begin());
  }

  const_reverse_iterator rend() const noexcept {
    return reverse_iterator(begin());
  }

  const_reverse_iterator crend() const noexcept {
    return reverse_iterator(cbegin());
  }

  // Finally some operations
  void push_back(const T & value) {
    insert_impl(sentinel(), value);
  }

  void push_back(T && value) {
    insert_impl(sentinel(), std::move(value));
  }

private:
  // helper functions
  node * sentinel() {
    return header_.as_node_ptr();
  }

  const node * sentinel() const {
    return header_.as_node_ptr();
  }

  template<typename ... Args>
  void insert_impl(node * where, Args && ... args) {
    node * x = new node(where, where->prev_, std::forward<Args>(args)...);
    where->prev_->next_ = x;
    where->prev_ = x;
  }
};

Sample usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "dl_list.h"
#include <iostream>

int main() {
  dl_list<int> list;
  list.push_back(42);
  list.push_back(53);

  dl_list<int> another(std::move(list));

  for (const auto & e: another) {
    std::cout << e << '\n';
  }

  for (auto it = another.rbegin(); it != another.rend(); ++it) {
    std::cout << *it << '\n';
  }

  std::cout << "Done!\n";
}