Doubly Linked List: list<T>

list00: data structure

node

Doubly linked list: list

list00.cc:

namespace leanstl
{

struct list_node_base
{
  list_node_base* next;
  list_node_base* prev;
};

template <typename T>
class list
{
 public:
  list()
  {
    head_.next = &head_;
    head_.prev = &head_;
  }

 private:
  struct list_node : list_node_base
  {
    T value_;
  };

  list_node_base head_;
  size_t size_ = 0;
};

}  // namespace leanstl

Object sizes on 64-bit Linux.

sizeof void*:           8
sizeof size_t:          8
sizeof list_node_base: 16
sizeof list:           24

T = int
     sizeof T:          4
     sizeof list_node: 24

T = std::string
     sizeof T:          8
     sizeof list_node: 24

T = std::vector<int>
     sizeof T:         24
     sizeof list_node: 40

Object sizes on 32-bit Linux.

sizeof void*:           4
sizeof size_t:          4
sizeof list_node_base:  8
sizeof list:           12

T = int
     sizeof T:          4
     sizeof list_node: 12

T = std::string
     sizeof T:          4
     sizeof list_node: 12

T = std::vector<int>
     sizeof T:         12
     sizeof list_node: 20

list01: destructor

template <typename T>
class list
{
 public:
  list()
  {
    head_.next = &head_;
    head_.prev = &head_;
  }

  ~list()
  {
    for (list_node_base* n = head_.next; n != &head_;)
    {
      list_node* tod = static_cast<list_node*>(n);
      n = n->next;
      delete tod;
    }
  }

  list(const list& rhs) = delete;
  void operator=(const list& rhs) = delete;

  // Tentative, so that we can test non-empty lists
  void push_front(T x)
  {
    list_node* n = new list_node(std::move(x));
    n->next = head_.next;
    n->next->prev = n;
    head_.next = n;
    n->prev = &head_;
    size_++;
  }

  T& front()
  {
    return static_cast<list_node*>(head_.next)->value_;
  }

 private:
  struct list_node : list_node_base
  {
    T value_;
    list_node(T&& x) : value_(std::move(x)) {}
  };

  list_node_base head_;
  size_t size_ = 0;
};

Mistake: use-after-free

list01err0.cc

  ~list()
  {
    // WRONG: use-after-free
    for (list_node_base* n = head_.next; n != &head_; n = n->next)
    {
      delete static_cast<list_node*>(n);
    }
  }
int main()
{
  leanstl::list<int> li;
  li.push_front(43);
  printf("front=%d\n", li.front());
}
$ code/bin/list01err0
front=43
Segmentation fault (core dumped)

$ gdb code/bin/list01err0 core
Core was generated by `code/bin/list01err0'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400847 in leanstl::list<int>::~list (this=0x7ffd3dff5da0) at list01err0.cc:30
30          for (list_node_base* n = head_.next; n != &head_; n = n->next)

(gdb) p n
$1 = (leanstl::list_node_base *) 0x0

(gdb) p *this
$2 = {
  head_ = {
    next = 0x181e010,
    prev = 0x181e010
  },
  size_ = 1
}

(gdb) p *head_.next
$3 = {
  next = 0x0,
  prev = 0x7ffd3dff5da0
}

Some clever person finds a way to avoid coredump, by deleting from tail to head.

list01err0hack.cc

  ~list()
  {
    // STILL WRONG: use-after-free
    for (list_node_base* n = head_.prev; n != &head_; n = n->prev)
    {
      delete static_cast<list_node*>(n);
    }
  }

Valgrind

==56399== Invalid read of size 8
==56399==    at 0x400847: leanstl::list<int>::~list() (list01err0.cc:30)
==56399==    by 0x4007C6: main (list01err0.cc:75)
==56399==  Address 0x5a02040 is 0 bytes inside a block of size 24 free'd
==56399==    at 0x4C2A360: operator delete(void*) (vg_replace_malloc.c:507)
==56399==    by 0x400842: leanstl::list<int>::~list() (list01err0.cc:32)
==56399==    by 0x4007C6: main (list01err0.cc:75)

Address sanitizer

$ bin/list01err0asan
front=43
=================================================================
==57074==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300000efe0 at pc 0x400de5 bp 0x7ffe324e9ee0 sp 0x7ffe324e9ed8
READ of size 8 at 0x60300000efe0 thread T0
    #0 0x400de4 in leanstl::list<int>::~list() code/list01err0.cc:30
    #1 0x400c3f in main code/list01err0.cc:73
    #2 0x7f19eb9f7b44 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b44)
    #3 0x400a78 (code/bin/list01err0asan+0x400a78)

0x60300000efe0 is located 0 bytes inside of 24-byte region [0x60300000efe0,0x60300000eff8)
freed by thread T0 here:
    #0 0x7f19ec5f8477 in operator delete(void*) (/usr/lib/x86_64-linux-gnu/libasan.so.1+0x55477)
    #1 0x400dc3 in leanstl::list<int>::~list() code/list01err0.cc:32
    #2 0x400c3f in main code/list01err0.cc:73
    #3 0x7f19eb9f7b44 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b44)

previously allocated by thread T0 here:
    #0 0x7f19ec5f7fff in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.1+0x54fff)
    #1 0x400e34 in leanstl::list<int>::push_front(int) code/list01err0.cc:41
    #2 0x400bda in main code/list01err0.cc:72
    #3 0x7f19eb9f7b44 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b44)

SUMMARY: AddressSanitizer: heap-use-after-free code/list01err0.cc:30 leanstl::list<int>::~list()

Mistake: delete pointer of list_node_base*

list01err1.cc

  ~list()
  {
    // WRONG: delete base pointer without virtual dtor
    for (list_node_base* n = head_.next; n != &head_;)
    {
      list_node_base* tod = n;
      n = n->next;
      delete tod;
    }
  }

TODO: quote standard on this undefined behavior.

Valgrind

==56456== HEAP SUMMARY:
==56456==     in use at exit: 30 bytes in 1 blocks
==56456==   total heap usage: 4 allocs, 3 frees, 102 bytes allocated
==56456==
==56456== LEAK SUMMARY:
==56456==    definitely lost: 30 bytes in 1 blocks

Address sanitizer (clang 3.9.0)

==57346==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 30 byte(s) in 1 object(s) allocated from:
    #0 0x4e7e7b  (code/bin/list01err1asan+0x4e7e7b)
    #1 0x7fa4984c1e98  (/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0xbee98)

SUMMARY: AddressSanitizer: 30 byte(s) leaked in 1 allocation(s).

Double mistake:

  ~list()
  {
    // DOUBLY WRONG
    for (list_node_base* n = head_.next; n != &head_; n = n->next)
    {
      delete n;
    }
  }

Execrise: try memory checker with this double error.

list01alt: list_node_base with protected destructor

struct list_node_base
{
  list_node_base* next;
  list_node_base* prev;

  // prevent deleting a pointer of list_node_base* which actually points to a list_node<T>
  // GCC had bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54812 on this, fixed in 4.9.0
 protected:
  ~list_node_base() = default;
};

template <typename T>
struct list_node : list_node_base
{
  T value_;
  list_node(T&& x) : value_(std::move(x)) {}
};

template <typename T>
class list
{
 public:
  list()
    : head_(0)
  {
    head_.next = &head_;
    head_.prev = &head_;
  }

 private:
  list_node<size_t> head_;  // size is head_.value_
};

TODO: Why not virtual dtor?

Site note: GCC Bug 54812

struct Base
{
 private:
  ~Base() = default;
};

int main()
{
  Base* p = new Base;
  delete p;
}

list02: iterator

list02.cc:

template <typename T>
class list
{
 public:
  class iterator
  {
   public:
    iterator(list_node_base* n = nullptr) : node_(n) {}
    T& operator*() { return *operator->(); }
    T* operator->() { return &static_cast<list_node*>(node_)->value_; }
    iterator& operator++() { node_ = node_->next; return *this; }
    iterator operator++(int) { iterator old = *this; operator++(); return old; }
    iterator& operator--() { node_ = node_->prev; return *this; }
    iterator operator--(int) { iterator old = *this; operator--(); return old; }
    bool operator==(iterator rhs) const { return node_ == rhs.node_; }
    bool operator!=(iterator rhs) const { return node_ != rhs.node_; }

   private:
    list_node_base* node_;
  };

  iterator begin() { return iterator(head_.next); }
  iterator end() { return iterator(&head_); }
};

list02const: const_iterator

list02const.cc:

  class const_iterator
  {
   public:
    const_iterator(const list_node_base* n = nullptr) : node_(n) {}
    const T& operator*() { return *operator->(); }
    const T* operator->() { return &static_cast<const list_node*>(node_)->value_; }
    const_iterator& operator++() { node_ = node_->next; return *this; }
    const_iterator operator++(int) { const_iterator old = *this; operator++(); return old; }
    const_iterator& operator--() { node_ = node_->prev; return *this; }
    const_iterator operator--(int) { const_iterator old = *this; operator--(); return old; }
    bool operator==(const_iterator rhs) const { return node_ == rhs.node_; }
    bool operator!=(const_iterator rhs) const { return node_ != rhs.node_; }

   private:
    const list_node_base* node_;
  };

CRTP ?

list03: copy control

list03.cc:

template <typename T>
class list
{
 public:
  list()
  {
    init_head();
  }

  list(const list& rhs) : list()
  {
    for (const T& x: rhs)
    {
      push_back(x);
    }
  }

  list(list&& rhs) : list() { take_over(rhs); }
  // Effective Modern C++, Item 41.
  list& operator=(list rhs) { swap(rhs); return *this; }

  void swap(list& rhs)
  {
    list tmp(std::move(rhs));
    rhs.take_over(*this);
    take_over(tmp);
  }

  void push_front(T x)
  {
    list_node* n = new list_node(std::move(x));
    insert_node(head_.next, n);
    size_++;
  }

  void push_back(const T& x)
  {
    list_node* n = new list_node(x);
    insert_node(&head_, n);
    size_++;
  }

 private:
  struct list_node : list_node_base
  {
    T value_;
    list_node(const T& x) : value_(x) {}
    list_node(T&& x) : value_(std::move(x)) {}
  };

  void insert_node(list_node_base* pos, list_node* n)
  {
    n->next = pos;
    n->prev = pos->prev;
    pos->prev->next = n;
    pos->prev = n;
  }

  void init_head()
  {
    head_.next = &head_;
    head_.prev = &head_;
  }

  void take_over(list& rhs)
  {
    assert(size_ == 0);
    assert(head_.next == &head_);
    assert(head_.prev == &head_);

    if (rhs.size_ > 0)
    {
      head_.next = rhs.head_.next;
      head_.prev = rhs.head_.prev;
      head_.next->prev = &head_;
      head_.prev->next = &head_;
      rhs.init_head();
      size_ = rhs.size_;
      rhs.size_ = 0;
    }
  }

  list_node_base head_;
  size_t size_ = 0;
};

Mistake

  list(const list& rhs)  // WRONG: head_ is uninitialized
  {
    for (const T& x: rhs)
    {
      push_back(x);
    }
  }

TODO: memory sanitizer and Valgrind

Composition vs. inheriance

class list_base
{
 protected:
  list_base()
  {
    init_head();
  }

  ~list_base()
  {
    if (size_ == 0)
    {
      assert(head_.next == &head_);
      assert(head_.prev == &head_);
    }
  }

  void init_head()
  {
    head_.next = &head_;
    head_.prev = &head_;
  }

  list_node_base head_;
  size_t size_ = 0;
};
template <typename T>
class list : private list_base
{
 public:
  list() {}

  list(const list& rhs)
  {
    for (const T& x: rhs)
    {
      push_back(x);
    }
  }

  list(list&& rhs) { take_over(rhs); }

list04: more constructors

  explicit list(size_t n)
  {
    for (size_t i = 0; i < n; ++i)
      insert_node(&head_, new list_node);
    size_ = n;
  }

  list(size_t n, const T& value)
  {
    for (size_t i = 0; i < n; ++i)
      push_back(value);
  }
  struct list_node : list_node_base
  {
    T value_;
    list_node() : value_() {}
    list_node(const T& x) : value_(x) {}
    list_node(T&& x) : value_(std::move(x)) {}
  };

Mistake

  struct list_node : list_node_base
  {
    T value_;
    list_node() {}  // WRONG: value_ is unitialized for primitive types
    list_node(const T& x) : value_(x) {}
    list_node(T&& x) : value_(std::move(x)) {}
  };

TODO: Valgrind

But memory sanitizer doesn't catch this.

InputIterator vs. Integer

  list(const list& rhs) : list(rhs.begin(), rhs.end()) {}

  template <typename InputIterator>
  list(InputIterator first, InputIterator last)
  {
    initialize(first, last, typename std::is_integral<InputIterator>::type());
  }

  list(std::initializer_list<T> il) : list(il.begin(), il.end()) {}

  template <typename InputIterator>
  void initialize(InputIterator first, InputIterator last, std::false_type)
  {
    for (; first != last; ++first)
      push_back(*first);
  }

  template <typename Integer>
  void initialize(Integer n, Integer value, std::true_type)
  {
    for (Integer i = 0; i < n; ++i)
      push_back(value);
  }