IntrusiveDList

IntrusiveDList is a class that provides a double linked list using pointers embedded in the object. IntrusiveDList also acts as a queue. No memory management is done - objects can be added to and removed from the list but the allocation and deallocation of the objects must be handled outside the class. This class supports an STL compliant bidirectional iteration. The iterators automatically convert to pointer as in normal use of this class the contained elements will be referenced by pointers.

Definition

template<typename L>
class IntrusiveDList

A double linked list / queue based on links inside the objects. The element type, T, is deduced from the return type of the link accessor methods in L.

  • Template Parameters

    L – List item descriptor

The descriptor, L, is a type that provides the operations on list elements required by the container.

  • type value_type

    The type of elements in the container, deduced from the return types of the link accessor methods in L.

  • L

    • static value_type *&next_ptr(value_type *elt)

      Return a reference to the next element pointer embedded in the element elt.

    • static value_type *&prev_ptr(value_type *elt)

      Return a reference to the previous element pointer embedded in the element elt.

  • type iterator

    An STL compliant bidirectional iterator on elements in the list. iterator has a user defined conversion to value_type * for convenience in use.

  • type const_iterator

    An STL compliant bidirectional constant iterator on elements in the list. const_iterator has a user defined conversion to const value_type * for convenience in use.

  • value_type *head()

    Return a pointer to the head element in the list. This may be nullptr if the list is empty.

  • value_type *tail()

    Return a pointer to the tail element in the list. This may be nullptr if the list is empty.

  • IntrusiveDList &clear()

    Remove all elements from the list. This only removes, no deallocation nor destruction is performed.

  • size_t count() const

    Return the number of elements in the list.

  • IntrusiveDList &append(value_type *elt)

    Append elt to the list.

  • IntrusiveDList &prepend(value_type *elt)

    Prepend elt to the list.

  • value_type *take_head()

    Remove the head element and return a pointer to it. May be nullptr if the list is empty.

  • value_type *take_tail()

    Remove the tail element and return a pointer to it. May be nullptr if the list is empty.

  • iterator erase(const iterator &loc)

    Remove the element at loc. Return the element after loc.

  • iterator erase(const iterator &start, const iterator &limit)

    Remove the elements in the half open range from and including start to but not including limit.

  • iterator iterator_for(value_type *value)

    Return an iterator that refers to value. value is checked for being in a list but there is no guarantee it is in this list. If value is not in a list then the end iterator is returned.

  • const_iterator iterator_for(const value_type *value)

    Return a const_iterator that refers to value. value is checked for being in a list but there is no guarantee it is in this list. If value is not in a list then the end iterator is returned.

Usage

An instance of IntrusiveDList acts as a container for items, maintaining a doubly linked list / queue of the objects and tracking the number of objects in the container. There are methods for appending, prepending, and inserting (both before and after a specific element already in the list). Some care must be taken because it is too expensive to check for an element already being in the list or in another list. The internal links are set to nullptr, therefore one simple check for being in a list is if either internal link is not nullptr. This requires initializing the internal links to nullptr.

Examples

In this example the goal is to have a list of Message objects. First the class is declared along with the internal linkage support.

  1. {
  2. using self_type = Message; ///< Self reference type.
  3. public:
  4. // Message severity level.
  5. enum Severity { LVL_DEBUG, LVL_INFO, LVL_WARN, LVL_ERROR };
  6. protected:
  7. std::string _text; // Text of the message.
  8. Severity _severity{LVL_DEBUG};
  9. int _indent{0}; // indentation level for display.
  10. // Intrusive list support.
  11. struct Linkage {
  12. static self_type *&next_ptr(self_type *); // Link accessor.
  13. static self_type *&prev_ptr(self_type *); // Link accessor.
  14. self_type *_next{nullptr}; // Forward link.
  15. self_type *_prev{nullptr}; // Backward link.
  16. } _link;
  17. bool is_in_list() const;
  18. friend class Container;
  19. };

The struct Linkage is used both to provide the descriptor to IntrusiveDList and to contain the link pointers. This isn’t necessary - the links could have been direct members and the implementation of the link accessor methods adjusted. Because the links are intended to be used only by a specific container class (Container) the struct is made protected.

The implementation of the link accessor methods.

  1. Message::Linkage::next_ptr(self_type *that) -> self_type *&
  2. {
  3. return that->_link._next;
  4. }
  5. auto
  6. Message::Linkage::prev_ptr(self_type *that) -> self_type *&
  7. {
  8. return that->_link._prev;
  9. }

A method to check if the message is in a list.

  1. Message::is_in_list() const
  2. {
  3. return _link._next || _link._prev;
  4. }

The container class for the messages could be implemented as

  1. {
  2. using self_type = Container;
  3. using MessageList = ts::IntrusiveDList<Message::Linkage>;
  4. public:
  5. ~Container();
  6. template <typename... Args> self_type &debug(std::string_view fmt, Args &&... args);
  7. size_t count() const;
  8. self_type &clear();
  9. Message::Severity max_severity() const;
  10. void print() const;
  11. protected:
  12. MessageList _msgs;
  13. };

The debug method takes a format string (fmt) and an arbitrary set of arguments, formats the arguments in to the string, and adds the new message to the list.

  1. template <typename... Args>
  2. auto
  3. Container::debug(std::string_view fmt, Args &&... args) -> self_type &
  4. {
  5. Message *msg = new Message;
  6. ts::bwprintv(msg->_text, fmt, std::forward_as_tuple(args...));
  7. msg->_severity = Message::LVL_DEBUG;
  8. _msgs.append(msg);
  9. return *this;
  10. }

The print method demonstrates the use of the range for loop on a list.

  1. Container::print() const
  2. {
  3. for (auto &&elt : _msgs) {
  4. std::cout << static_cast<unsigned int>(elt._severity) << ": " << elt._text << std::endl;
  5. }
  6. }

The maximum severity level can also be computed even more easily using std::max_element. This find the element with the maximum severity and returns that severity, or LVL_DEBUG if no element is found (which happens if the list is empty).

  1. Container::max_severity() const
  2. {
  3. auto spot = std::max_element(_msgs.begin(), _msgs.end(),
  4. [](Message const &lhs, Message const &rhs) { return lhs._severity < rhs._severity; });
  5. return spot == _msgs.end() ? Message::Severity::LVL_DEBUG : spot->_severity;
  6. }

Other methods for the various severity levels would be implemented in a similar fashion. Because the intrusive list does not do memory management, the container must clean that up itself, as in the clear method. A bit of care must be exercised because the links are in the elements, and these links are used for iteration therefore using an iterator that references a deleted object is risky. One approach, illustrated here, is to use IntrusiveDList::take_head() to remove the element before destroying it. Another option is to allocation the elements in a MemArena to avoid the need for any explicit cleanup.

  1. Container::clear() -> self_type &
  2. {
  3. Message *msg;
  4. while (nullptr != (msg = _msgs.take_head())) {
  5. delete msg;
  6. }
  7. _msgs.clear();
  8. return *this;
  9. }

In some cases the elements of the list are subclasses and the links are declared in a super class and are therefore of the super class type. For instance, in the unit test a class Thing is defined for testing.

  1. Thing *_next{nullptr};

Later on, to validate use on a subclass, PrivateThing is defined as a subclass of Thing.

  1. {

However, the link members _next and _prev are of type Thing* but the descriptor for a list of PrivateThing must have link accessors that return PrivateThing *&. To make this easier a conversion template function is provided, ts::ptr_ref_cast<X, T> that converts a member of type T* to a reference to a pointer to X, e.g. X*&. This is used in the setup for testing PrivateThing.

  1. next_ptr(self_type *t)
  2. {
  3. return ts::ptr_ref_cast<self_type>(t->_next);
  4. }
  5. static self_type *&
  6. prev_ptr(self_type *t)
  7. {
  8. return ts::ptr_ref_cast<self_type>(t->_prev);
  9. }
  10. };

While this can be done directly with reinterpret_cast<>, use of ts::ptr_cast avoids typographic errors and warnings about type punning caused by -fstrict-aliasing.

Design Notes

The historic goal of this class is to replace the DLL list support. The benefits of this are

  • Remove dependency on the C preprocessor.

  • Provide greater flexibility in the internal link members. Because of the use of the descriptor and its static methods, the links can be anywhere in the object, including in nested structures or super classes. The links are declared like normal members and do not require specific macros.

  • Provide STL compliant iteration. This makes the class easier to use in general and particularly in the case of range for loops.

  • Track the number of items in the list.

  • Provide queue support, which is of such low marginal expense there is, IMHO, no point in providing a separate class for it.