pw_containers: Allow derived Item<T> for IntrusiveList

- Update IntrusiveList to support declaring an IntrusiveList with
  classes that derive from another class that inherits from Item<T>.
- Support comparing const and non-const iterators.
- Add tests, including some compilation failure tests.
- Make some Item member functions private.

Change-Id: Iea99c358f65b8abd1d78f240a466475dfcfd7929
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/48721
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Keir Mierle <keir@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_containers/docs.rst b/pw_containers/docs.rst
index f875ce4..51b224b 100644
--- a/pw_containers/docs.rst
+++ b/pw_containers/docs.rst
@@ -18,49 +18,36 @@
 their maximum size at compile time. It also keeps code size small since
 function implementations are shared for all maximum sizes.
 
-
 pw::IntrusiveList
 =================
-IntrusiveList provides an embedded-friendly singly-linked list implementation.
-An intrusive list is a type of linked list that embeds the "next" pointer into
-the list object itself. This allows the construction of a linked list without
-the need to dynamically allocate list entries to point to the actual in-memory
-objects. In C, an intrusive list can be made by manually including the "next"
-pointer as a member of the object's struct. `pw::IntrusiveList` uses C++
-features to simplify the process of creating an intrusive list and intrusive
-list objects by providing a class that list elements can inherit from. This
-protects the "next" pointer from being accessed by the actual item that is
-stored in the linked list; only the `pw::IntrusiveList` class can modify the
-list.
+IntrusiveList provides an embedded-friendly singly-linked intrusive list
+implementation. An intrusive list is a type of linked list that embeds the
+"next" pointer into the list object itself. This allows the construction of a
+linked list without the need to dynamically allocate list entries.
 
-
-pw::containers::FlatMap
-=======================
-FlatMap provides a simple, fixed-size associative array with lookup by key or
-value. ``pw::containers::FlatMap`` contains the same methods and features for
-looking up data as std::map. However, there are no methods that modify the
-underlying data.  The underlying array in ``pw::containers::FlatMap`` does not
-need to be sorted. During construction, ``pw::containers::FlatMap`` will
-perform a constexpr insertion sort.
-
+In C, an intrusive list can be made by manually including the "next" pointer as
+a member of the object's struct. ``pw::IntrusiveList`` uses C++ features to
+simplify the process of creating an intrusive list. ``pw::IntrusiveList``
+provides a class that list elements can inherit from. This protects the "next"
+pointer from being accessed by the item class, so only the ``pw::IntrusiveList``
+class can modify the list.
 
 Usage
 -----
-While the API of `pw::IntrusiveList` is relatively similar to a
-``std::forward_list``, there are extra steps to creating objects that can be
-stored in this data structure. Objects that will be added to a
-``IntrusiveList<T>`` must inherit from ``IntrusiveList<T>::Item``. When an item
-is instantiated and added to a linked list, the pointer to the object is added
-to the "next" pointer of whichever object is the current tail.
-
+While the API of ``pw::IntrusiveList`` is similar to a ``std::forward_list``,
+there are extra steps to creating objects that can be stored in this data
+structure. Objects that will be added to a ``IntrusiveList<T>`` must inherit
+from ``IntrusiveList<T>::Item``. They can inherit directly from it or inherit
+from it through another base class. When an item is instantiated and added to a
+linked list, the pointer to the object is added to the "next" pointer of
+whichever object is the current tail.
 
 That means two key things:
 
- - An instantiated IntrusiveList::Item must remain in scope for the lifetime of
-   the IntrusiveList it has been added to.
- - A linked list item CANNOT be included in two lists, as it is part of a
-   preexisting list and adding it to another implicitly breaks correctness
-   of the first list.
+ - An instantiated ``IntrusiveList<T>::Item`` must remain in scope for the
+   lifetime of the ``IntrusiveList`` it has been added to.
+ - A linked list item CANNOT be included in two lists. Attempting to do so
+   results in an assert failure.
 
 .. code-block:: cpp
 
@@ -85,15 +72,23 @@
   squares.push_back(large);
 
   {
-    // ERROR: When this goes out of scope, it will break the linked list.
+    // When different_scope goes out of scope, it removes itself from the list.
     Square different_scope = Square(5);
     squares.push_back(&different_scope);
   }
 
-  for (auto& square : squares) {
-    PW_LOG_INFO("Found a square with an area of %ul", square.Area());
+  for (const auto& square : squares) {
+    PW_LOG_INFO("Found a square with an area of %lu", square.Area());
   }
 
+pw::containers::FlatMap
+=======================
+FlatMap provides a simple, fixed-size associative array with lookup by key or
+value. ``pw::containers::FlatMap`` contains the same methods and features for
+looking up data as std::map. However, there are no methods that modify the
+underlying data.  The underlying array in ``pw::containers::FlatMap`` does not
+need to be sorted. During construction, ``pw::containers::FlatMap`` will
+perform a constexpr insertion sort.
 
 Compatibility
 =============
diff --git a/pw_containers/intrusive_list.cc b/pw_containers/intrusive_list.cc
index e923684..c005b50 100644
--- a/pw_containers/intrusive_list.cc
+++ b/pw_containers/intrusive_list.cc
@@ -18,8 +18,6 @@
 
 namespace pw::intrusive_list_impl {
 
-List::Item::~Item() { unlist(); }
-
 void List::Item::unlist(Item* prev) {
   if (prev == nullptr) {
     prev = previous();
diff --git a/pw_containers/intrusive_list_test.cc b/pw_containers/intrusive_list_test.cc
index 9f56c1d..6a5c45f 100644
--- a/pw_containers/intrusive_list_test.cc
+++ b/pw_containers/intrusive_list_test.cc
@@ -376,10 +376,26 @@
   }
 }
 
+TEST(IntrusiveList, CompareConstAndNonConstIterator) {
+  IntrusiveList<TestItem> list;
+  EXPECT_EQ(list.end(), list.cend());
+}
+
+#if defined(PW_COMPILE_FAIL_TEST_incompatible_iterator_types)
+
+struct OtherItem : public IntrusiveList<OtherItem>::Item {};
+
+TEST(IntrusiveList, CompareConstAndNonConstIterator_CompilationFails) {
+  IntrusiveList<TestItem> list;
+  IntrusiveList<OtherItem> list2;
+  static_cast<void>(list.end() == list2.end());
+}
+
+#endif
+
 // TODO(pwbug/47): These tests should fail to compile, enable when no-compile
 // tests are set up in Pigweed.
-#define NO_COMPILE_TESTS 0
-#if NO_COMPILE_TESTS
+#if defined(PW_COMPILE_FAIL_TEST_cannot_modify_through_const_iterator)
 TEST(IntrusiveList, ConstIteratorModify) {
   TestItem item1(1);
   TestItem item2(99);
@@ -396,7 +412,7 @@
     it++;
   }
 }
-#endif  // NO_COMPILE_TESTS
+#endif  // Compile failure test
 
 // TODO(pwbug/88): These tests should trigger a CHECK failure. This requires
 // using a testing version of pw_assert.
@@ -603,5 +619,51 @@
   EXPECT_EQ(list.size(), static_cast<size_t>(0));
 }
 
+// Test that a list of items derived from a different Item class can be created.
+class DerivedTestItem : public TestItem {};
+
+TEST(InstrusiveList, AddItemsOfDerivedClassToList) {
+  IntrusiveList<TestItem> list;
+
+  DerivedTestItem item1;
+  list.push_front(item1);
+
+  TestItem item2;
+  list.push_front(item2);
+
+  EXPECT_EQ(2u, list.size());
+}
+
+TEST(InstrusiveList, ListOfDerivedClassItems) {
+  IntrusiveList<DerivedTestItem> derived_from_compatible_item_type;
+
+  DerivedTestItem item1;
+  derived_from_compatible_item_type.push_front(item1);
+
+  EXPECT_EQ(1u, derived_from_compatible_item_type.size());
+
+// TODO(pwbug/47): Make these proper automated compilation failure tests.
+#if defined(PW_COMPILE_FAIL_TEST_cannot_add_base_class_to_derived_class_list)
+  TestItem item2;
+  derived_from_compatible_item_type.push_front(item2);
+#endif
+}
+
+#if defined(PW_COMPILE_FAIL_TEST_incompatibile_item_type)
+
+struct Foo {};
+
+class BadItem : public IntrusiveList<Foo>::Item {};
+
+[[maybe_unused]] IntrusiveList<BadItem> derived_from_incompatible_item_type;
+
+#elif defined(PW_COMPILE_FAIL_TEST_does_not_inherit_from_item)
+
+struct NotAnItem {};
+
+[[maybe_unused]] IntrusiveList<NotAnItem> list;
+
+#endif
+
 }  // namespace
 }  // namespace pw
diff --git a/pw_containers/public/pw_containers/internal/intrusive_list_impl.h b/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
index 13bd448..ab020a8 100644
--- a/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
+++ b/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
@@ -14,6 +14,7 @@
 #pragma once
 
 #include <iterator>
+#include <type_traits>
 
 namespace pw {
 
@@ -50,14 +51,20 @@
   constexpr const T* operator->() const { return static_cast<T*>(item_); }
   constexpr T* operator->() { return static_cast<T*>(item_); }
 
-  constexpr bool operator==(const Iterator& rhs) const {
+  template <typename U, typename J>
+  constexpr bool operator==(const Iterator<U, J>& rhs) const {
     return item_ == rhs.item_;
   }
-  constexpr bool operator!=(const Iterator& rhs) const {
+
+  template <typename U, typename J>
+  constexpr bool operator!=(const Iterator<U, J>& rhs) const {
     return item_ != rhs.item_;
   }
 
  private:
+  template <typename, typename>
+  friend class Iterator;
+
   template <typename>
   friend class ::pw::IntrusiveList;
 
@@ -73,15 +80,7 @@
    protected:
     constexpr Item() : Item(this) {}
 
-    bool unlisted() const { return this == next_; }
-
-    // Unlink this from the list it is apart of, if any. Specifying prev saves
-    // calling previous(), which requires looping around the cycle.
-    void unlist(Item* prev = nullptr);
-
-    Item* previous();  // Note: O(n) since it loops around the cycle.
-
-    ~Item();
+    ~Item() { unlist(); }
 
    private:
     friend class List;
@@ -91,6 +90,14 @@
 
     constexpr Item(Item* next) : next_(next) {}
 
+    bool unlisted() const { return this == next_; }
+
+    // Unlink this from the list it is apart of, if any. Specifying prev saves
+    // calling previous(), which requires looping around the cycle.
+    void unlist(Item* prev = nullptr);
+
+    Item* previous();  // Note: O(n) since it loops around the cycle.
+
     // The next pointer. Unlisted items must be self-cycles (next_ == this).
     Item* next_;
   };
@@ -161,5 +168,21 @@
   }
 }
 
+// Gets the element type from an Item. This is used to check that an
+// IntrusiveList element class inherits from Item, either directly or through
+// another class.
+template <typename T, bool kIsItem = std::is_base_of<List::Item, T>()>
+struct GetListElementTypeFromItem {
+  using Type = void;
+};
+
+template <typename T>
+struct GetListElementTypeFromItem<T, true> {
+  using Type = typename T::PwIntrusiveListElementType;
+};
+
+template <typename T>
+using ElementTypeFromItem = typename GetListElementTypeFromItem<T>::Type;
+
 }  // namespace intrusive_list_impl
 }  // namespace pw
diff --git a/pw_containers/public/pw_containers/intrusive_list.h b/pw_containers/public/pw_containers/intrusive_list.h
index b73effe..b7e800f 100644
--- a/pw_containers/public/pw_containers/intrusive_list.h
+++ b/pw_containers/public/pw_containers/intrusive_list.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2021 The Pigweed Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
 // use this file except in compliance with the License. You may obtain a copy of
@@ -55,6 +55,14 @@
   class Item : public intrusive_list_impl::List::Item {
    protected:
     constexpr Item() = default;
+
+   private:
+    // GetListElementTypeFromItem is used to find the element type from an item.
+    // It is used to ensure list items inherit from the correct Item type.
+    template <typename, bool>
+    friend struct intrusive_list_impl::GetListElementTypeFromItem;
+
+    using PwIntrusiveListElementType = T;
   };
 
   using element_type = T;
@@ -154,8 +162,9 @@
   // defined when the IntrusiveList<T> class is instantiated.
   static constexpr void CheckItemType() {
     static_assert(
-        std::is_base_of<Item, T>(),
-        "IntrusiveList items must be derived from IntrusiveList<T>::Item");
+        std::is_base_of<intrusive_list_impl::ElementTypeFromItem<T>, T>(),
+        "IntrusiveList items must be derived from IntrusiveList<T>::Item, "
+        "where T is the item or one of its bases.");
   }
 
   intrusive_list_impl::List list_;