pw_containers: IntrusiveList updates
- Use an Item for List::head_ and have end() be &head_ instead of
nullptr. This makes it possible to detect if an item is already in a
list. Previously, checking nullptr was insufficient becuase the last
item in the list pointed to nullptr. This approach also greatly
simplifies the logic for adding and removing items.
- Add a destructor that calls clear().
- Allow constructing and assigning from iterators and initializer lists,
and delete the copy constructor.
- Add erase_after and remove, which removes a specific item.
- Expand tests.
Change-Id: I392cdeca31e6dca5b3d29542d79636cfd996ca87
diff --git a/pw_containers/intrusive_list.cc b/pw_containers/intrusive_list.cc
index 20c6686..0ddfbe8 100644
--- a/pw_containers/intrusive_list.cc
+++ b/pw_containers/intrusive_list.cc
@@ -18,74 +18,44 @@
namespace pw::intrusive_list_impl {
-void List::push_back(Item& item) {
- // The incoming item's next is always nullptr, because item is added at the
- // end of the list.
- PW_CHECK_PTR_EQ(item.next_, nullptr);
-
- if (head_ == nullptr) {
- head_ = &item;
- return;
- }
-
- Item* current = head_;
-
- while (current->next_ != nullptr) {
- current = current->next_;
- }
-
- current->next_ = &item;
-}
-
-List::Item& List::insert_after(Item* pos, Item& item) {
- if (pos == nullptr) {
- push_back(item);
- return item;
- }
-
- // If `next_` of the incoming element is not null, the item is in use and
- // can't be added to this list.
- PW_CHECK_PTR_EQ(item.next_,
- nullptr,
- "Cannot add an item to a pw::IntrusiveList when it "
- "exists in another list");
+void List::insert_after(Item* pos, Item& item) {
+ PW_CHECK_PTR_EQ(
+ item.next_,
+ nullptr,
+ "Cannot add an item to a pw::IntrusiveList that is already in a list");
item.next_ = pos->next_;
pos->next_ = &item;
- return item;
}
-void List::push_front(Item& item) {
- // If `next_` of the incoming element is not null, the item is in use and
- // can't be added to this list.
- PW_CHECK_PTR_EQ(item.next_,
- nullptr,
- "Cannot add an item to a pw::IntrusiveList when it "
- "exists in another list");
- item.next_ = head_;
- head_ = &item;
+void List::erase_after(Item* pos) {
+ Item* const item_to_remove = pos->next_;
+ pos->next_ = item_to_remove->next_;
+ item_to_remove->next_ = nullptr;
}
-void List::pop_front() {
- if (head_ == nullptr) {
- return;
+List::Item* List::before_end() noexcept {
+ Item* pos = before_begin();
+ while (pos->next_ != end()) {
+ pos = pos->next_;
}
- Item* old_head = head_;
- head_ = head_->next_;
- old_head->next_ = nullptr;
+ return pos;
}
void List::clear() {
- if (head_ == nullptr) {
- return;
+ while (!empty()) {
+ erase_after(before_begin());
+ }
+}
+
+bool List::remove(const Item& item_to_remove) {
+ for (Item* pos = before_begin(); pos->next_ != end(); pos = pos->next_) {
+ if (pos->next_ == &item_to_remove) {
+ erase_after(pos);
+ return true;
+ }
}
- while (head_->next_ != nullptr) {
- Item* item_to_remove = head_->next_;
- head_->next_ = item_to_remove->next_;
- item_to_remove->next_ = nullptr;
- }
-
- head_ = nullptr;
+ return false;
}
} // namespace pw::intrusive_list_impl
diff --git a/pw_containers/intrusive_list_test.cc b/pw_containers/intrusive_list_test.cc
index 8f7dc5a..3d666e4 100644
--- a/pw_containers/intrusive_list_test.cc
+++ b/pw_containers/intrusive_list_test.cc
@@ -14,6 +14,7 @@
#include "pw_containers/intrusive_list.h"
+#include <array>
#include <cstddef>
#include <cstdint>
@@ -27,13 +28,118 @@
public:
TestItem() : number_(0) {}
TestItem(int number) : number_(number) {}
+
int GetNumber() const { return number_; }
void SetNumber(int num) { number_ = num; }
+ // Add equality comparison to ensure comparisons are done by identity rather
+ // than equality for the remove function.
+ bool operator==(const TestItem& other) const {
+ return number_ == other.number_;
+ }
+
private:
int number_;
};
+TEST(IntrusiveList, Construct_InitializerList_Empty) {
+ IntrusiveList<TestItem> list({});
+ EXPECT_TRUE(list.empty());
+}
+
+TEST(IntrusiveList, Construct_InitializerList_One) {
+ TestItem one(1);
+ IntrusiveList<TestItem> list({&one});
+
+ EXPECT_EQ(&one, &list.front());
+}
+
+TEST(IntrusiveList, Construct_InitializerList_Multiple) {
+ TestItem one(1);
+ TestItem two(2);
+ TestItem thr(3);
+
+ IntrusiveList<TestItem> list({&one, &two, &thr});
+ auto it = list.begin();
+ EXPECT_EQ(&one, &(*it++));
+ EXPECT_EQ(&two, &(*it++));
+ EXPECT_EQ(&thr, &(*it++));
+ EXPECT_EQ(list.end(), it);
+}
+
+TEST(IntrusiveList, Construct_ObjectIterator_Empty) {
+ std::array<TestItem, 0> array;
+ IntrusiveList<TestItem> list(array.begin(), array.end());
+
+ EXPECT_TRUE(list.empty());
+}
+
+TEST(IntrusiveList, Construct_ObjectIterator_One) {
+ std::array<TestItem, 1> array{{{1}}};
+ IntrusiveList<TestItem> list(array.begin(), array.end());
+
+ EXPECT_EQ(&array.front(), &list.front());
+}
+
+TEST(IntrusiveList, Construct_ObjectIterator_Multiple) {
+ std::array<TestItem, 3> array{{{1}, {2}, {3}}};
+
+ IntrusiveList<TestItem> list(array.begin(), array.end());
+ auto it = list.begin();
+ EXPECT_EQ(&array[0], &(*it++));
+ EXPECT_EQ(&array[1], &(*it++));
+ EXPECT_EQ(&array[2], &(*it++));
+ EXPECT_EQ(list.end(), it);
+}
+
+TEST(IntrusiveList, Construct_PointerIterator_Empty) {
+ std::array<TestItem*, 0> array;
+ IntrusiveList<TestItem> list(array.begin(), array.end());
+
+ EXPECT_TRUE(list.empty());
+}
+
+TEST(IntrusiveList, Construct_PointerIterator_One) {
+ std::array<TestItem, 1> array{{{1}}};
+ std::array<TestItem*, 1> ptrs{{&array[0]}};
+
+ IntrusiveList<TestItem> list(ptrs.begin(), ptrs.end());
+
+ EXPECT_EQ(ptrs[0], &list.front());
+}
+
+TEST(IntrusiveList, Construct_PointerIterator_Multiple) {
+ std::array<TestItem, 3> array{{{1}, {2}, {3}}};
+ std::array<TestItem*, 3> ptrs{{&array[0], &array[1], &array[2]}};
+
+ IntrusiveList<TestItem> list(ptrs.begin(), ptrs.end());
+ auto it = list.begin();
+ EXPECT_EQ(ptrs[0], &(*it++));
+ EXPECT_EQ(ptrs[1], &(*it++));
+ EXPECT_EQ(ptrs[2], &(*it++));
+ EXPECT_EQ(list.end(), it);
+}
+
+TEST(IntrusiveList, Assign_ReplacesPriorContents) {
+ std::array<TestItem, 3> array{{{0}, {100}, {200}}};
+ IntrusiveList<TestItem> list(array.begin(), array.end());
+
+ list.assign(array.begin() + 1, array.begin() + 2);
+
+ auto it = list.begin();
+ EXPECT_EQ(&array[1], &(*it++));
+ EXPECT_EQ(list.end(), it);
+}
+
+TEST(IntrusiveList, Assign_EmptyRange) {
+ std::array<TestItem, 3> array{{{0}, {100}, {200}}};
+ IntrusiveList<TestItem> list(array.begin(), array.end());
+
+ list.assign(array.begin() + 1, array.begin() + 1);
+
+ EXPECT_TRUE(list.empty());
+}
+
TEST(IntrusiveList, PushOne) {
constexpr int kMagicValue = 31;
IntrusiveList<TestItem> test_items;
@@ -216,8 +322,6 @@
TEST(IntrusiveList, ListFront) {
IntrusiveList<TestItem> test_items;
- EXPECT_EQ(&test_items.front(), nullptr);
-
TestItem item1(1);
TestItem item2(0);
TestItem item3(0xffff);
@@ -287,7 +391,131 @@
it++;
}
}
+
#endif // NO_COMPILE_TESTS
+// TODO(pwbug/88): These tests should trigger a CHECK failure. This requires
+// using a testing version of pw_assert.
+#if TESTING_CHECK_FAILURES_IS_SUPPORTED
+
+TEST(IntrusiveList, Construct_DuplicateItems) {
+ TestItem item(1);
+ IntrusiveList<TestItem> list({&item, &item});
+}
+
+TEST(IntrusiveList, InsertAfter_SameItem) {
+ TestItem item(1);
+ IntrusiveList<TestItem> list({&item});
+
+ list.insert_after(list.begin(), item);
+}
+
+TEST(IntrusiveList, InsertAfter_SameItemAfterEnd) {
+ TestItem item(1);
+ IntrusiveList<TestItem> list({&item});
+
+ list.insert_after(list.end(), item);
+}
+
+TEST(IntrusiveList, PushBack_SameItem) {
+ TestItem item(1);
+ IntrusiveList<TestItem> list({&item});
+
+ list.push_back(item);
+}
+
+TEST(IntrusiveList, PushFront_SameItem) {
+ TestItem item(1);
+ IntrusiveList<TestItem> list({&item});
+
+ list.push_front(item);
+}
+
+#endif // TESTING_CHECK_FAILURES_IS_SUPPORTED
+
+TEST(IntrusiveList, EraseAfter_FirstItem) {
+ std::array<TestItem, 3> items{{{0}, {1}, {2}}};
+ IntrusiveList<TestItem> list(items.begin(), items.end());
+
+ auto it = list.erase_after(list.before_begin());
+ EXPECT_EQ(list.begin(), it);
+ EXPECT_EQ(&items[1], &list.front());
+}
+
+TEST(IntrusiveList, EraseAfter_LastItem) {
+ std::array<TestItem, 3> items{{{0}, {1}, {2}}};
+ IntrusiveList<TestItem> list(items.begin(), items.end());
+
+ auto it = list.begin();
+ ++it;
+
+ it = list.erase_after(it);
+ EXPECT_EQ(list.end(), it);
+
+ it = list.begin();
+ ++it;
+
+ EXPECT_EQ(&items[1], &(*it));
+}
+
+TEST(IntrusiveList, EraseAfter_AllItems) {
+ std::array<TestItem, 3> items{{{0}, {1}, {2}}};
+ IntrusiveList<TestItem> list(items.begin(), items.end());
+
+ list.erase_after(list.begin());
+ list.erase_after(list.begin());
+ auto it = list.erase_after(list.before_begin());
+
+ EXPECT_EQ(list.end(), it);
+ EXPECT_TRUE(list.empty());
+}
+
+TEST(IntrusiveList, Remove_EmptyList) {
+ std::array<TestItem, 1> items{{{3}}};
+ IntrusiveList<TestItem> list(items.begin(), items.begin()); // Add nothing!
+
+ EXPECT_TRUE(list.empty());
+ EXPECT_FALSE(list.remove(items[0]));
+}
+
+TEST(IntrusiveList, Remove_SingleItem_NotPresent) {
+ std::array<TestItem, 1> items{{{1}}};
+ IntrusiveList<TestItem> list(items.begin(), items.end());
+
+ EXPECT_FALSE(list.remove(TestItem(1)));
+ EXPECT_EQ(&items.front(), &list.front());
+}
+
+TEST(IntrusiveList, Remove_SingleItem_Removed) {
+ std::array<TestItem, 1> items{{{1}}};
+ IntrusiveList<TestItem> list(items.begin(), items.end());
+
+ EXPECT_TRUE(list.remove(items[0]));
+ EXPECT_TRUE(list.empty());
+}
+
+TEST(IntrusiveList, Remove_MultipleItems_NotPresent) {
+ std::array<TestItem, 5> items{{{1}, {1}, {2}, {3}, {4}}};
+ IntrusiveList<TestItem> list(items.begin(), items.end());
+
+ EXPECT_FALSE(list.remove(TestItem(1)));
+}
+
+TEST(IntrusiveList, Remove_MultipleItems_RemoveAndPushBack) {
+ std::array<TestItem, 5> items{{{1}, {1}, {2}, {3}, {4}}};
+ IntrusiveList<TestItem> list(items.begin(), items.end());
+
+ EXPECT_TRUE(list.remove(items[0]));
+ EXPECT_TRUE(list.remove(items[3]));
+ list.push_back(items[0]); // Make sure can add the item after removing it.
+
+ auto it = list.begin();
+ EXPECT_EQ(&items[1], &(*it++));
+ EXPECT_EQ(&items[2], &(*it++));
+ EXPECT_EQ(&items[4], &(*it++));
+ EXPECT_EQ(&items[0], &(*it++));
+ EXPECT_EQ(list.end(), it);
+}
+
} // 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 067187b..fe7e07d 100644
--- a/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
+++ b/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
@@ -15,15 +15,23 @@
#include <iterator>
-namespace pw::intrusive_list_impl {
+namespace pw {
+
+template <typename>
+class IntrusiveList;
+
+namespace intrusive_list_impl {
template <typename T, typename I>
class Iterator {
public:
+ using difference_type = void;
+ using value_type = std::remove_cv_t<T>;
+ using pointer = T*;
+ using reference = T&;
using iterator_category = std::forward_iterator_tag;
constexpr explicit Iterator() : item_(nullptr) {}
- constexpr explicit Iterator(I* item) : item_{item} {}
constexpr Iterator& operator++() {
item_ = static_cast<I*>(item_->next_);
@@ -50,6 +58,12 @@
}
private:
+ template <typename>
+ friend class ::pw::IntrusiveList;
+
+ // Only allow IntrusiveList to create iterators that point to something.
+ constexpr explicit Iterator(I* item) : item_{item} {}
+
I* item_;
};
@@ -57,7 +71,7 @@
public:
class Item {
protected:
- constexpr Item() : next_(nullptr) {}
+ constexpr Item() : Item(nullptr) {}
private:
friend class List;
@@ -65,27 +79,76 @@
template <typename T, typename I>
friend class Iterator;
+ constexpr Item(Item* next) : next_(next) {}
+
Item* next_;
};
- constexpr List() : head_(nullptr) {}
+ constexpr List() : head_(end()) {}
- void push_back(Item& item);
+ template <typename Iterator>
+ List(Iterator first, Iterator last) : List() {
+ AssignFromIterator(first, last);
+ }
- Item& insert_after(Item* pos, Item& item);
+ // Intrusive lists cannot be copied, since each Item can only be in one list.
+ List(const List&) = delete;
+ List& operator=(const List&) = delete;
- void push_front(Item& item);
+ ~List() { clear(); }
- void pop_front();
+ template <typename Iterator>
+ void assign(Iterator first, Iterator last) {
+ clear();
+ AssignFromIterator(first, last);
+ }
+
+ bool empty() const noexcept { return begin() == end(); }
+
+ static void insert_after(Item* pos, Item& item);
+
+ static void erase_after(Item* pos);
void clear();
- Item* begin() noexcept { return head_; }
- const Item* begin() const noexcept { return head_; }
- const Item* cbegin() const noexcept { return head_; }
+ bool remove(const Item& item_to_remove);
+
+ constexpr Item* before_begin() noexcept { return &head_; }
+ constexpr const Item* before_begin() const noexcept { return &head_; }
+
+ constexpr Item* begin() noexcept { return head_.next_; }
+ constexpr const Item* begin() const noexcept { return head_.next_; }
+
+ Item* before_end() noexcept;
+
+ constexpr Item* end() noexcept { return &head_; }
+ constexpr const Item* end() const noexcept { return &head_; }
private:
- Item* head_;
+ template <typename Iterator>
+ void AssignFromIterator(Iterator first, Iterator last);
+
+ // Use an Item for the head pointer. This gives simpler logic for inserting
+ // elements compared to using an Item*. It also makes it possible to use
+ // &head_ for end(), rather than nullptr. This makes end() unique for each
+ // List and ensures that items already in a list cannot be added to another.
+ Item head_;
};
-} // namespace pw::intrusive_list_impl
+template <typename Iterator>
+void List::AssignFromIterator(Iterator first, Iterator last) {
+ Item* current = &head_;
+
+ for (Iterator it = first; it != last; ++it) {
+ if constexpr (std::is_pointer<std::remove_reference_t<decltype(*it)>>()) {
+ insert_after(current, **it);
+ current = *it;
+ } else {
+ insert_after(current, *it);
+ current = &(*it);
+ }
+ }
+}
+
+} // 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 a9a783a..cba6c8c 100644
--- a/pw_containers/public/pw_containers/intrusive_list.h
+++ b/pw_containers/public/pw_containers/intrusive_list.h
@@ -14,6 +14,7 @@
#pragma once
#include <cstddef>
+#include <initializer_list>
#include <type_traits>
#include "pw_containers/internal/intrusive_list_impl.h"
@@ -51,7 +52,7 @@
class IntrusiveList {
public:
class Item : public intrusive_list_impl::List::Item {
- public:
+ protected:
constexpr Item() = default;
};
@@ -63,35 +64,84 @@
using const_iterator =
intrusive_list_impl::Iterator<std::add_const_t<T>, const Item>;
- [[nodiscard]] bool empty() const noexcept { return list_.begin() == nullptr; }
+ constexpr IntrusiveList() = default;
- void push_back(T& item) { list_.push_back(item); }
+ // Constructs an IntrusiveList from an iterator over Items. The iterator may
+ // dereference as either Item& (e.g. from std::array<Item>) or Item* (e.g.
+ // from std::initializer_list<Item*>).
+ template <typename Iterator>
+ IntrusiveList(Iterator first, Iterator last) : list_(first, last) {}
- void push_front(T& item) { list_.push_front(item); }
+ // Constructs an IntrusiveList from a std::initializer_list of pointers to
+ // items.
+ IntrusiveList(std::initializer_list<Item*> items)
+ : list_(items.begin(), items.end()) {}
- iterator insert_after(iterator pos, T& item) {
- return iterator(static_cast<Item*>(&list_.insert_after(&(*pos), item)));
+ template <typename Iterator>
+ void assign(Iterator first, Iterator last) {
+ list_.assign(first, last);
}
- void pop_front() { list_.pop_front(); }
+ void assign(std::initializer_list<Item*> items) {
+ list_.assign(items.begin(), items.end());
+ }
+ [[nodiscard]] bool empty() const noexcept { return list_.empty(); }
+
+ void push_front(T& item) { list_.insert_after(list_.before_begin(), item); }
+
+ void push_back(T& item) { list_.insert_after(list_.before_end(), item); }
+
+ iterator insert_after(iterator pos, T& item) {
+ list_.insert_after(&(*pos), item);
+ return iterator(&item);
+ }
+
+ // Removes the first item in the list. The list must not be empty.
+ void pop_front() { list_.erase_after(list_.before_begin()); }
+
+ // Removes the item following pos from the list. The item is not destructed.
+ iterator erase_after(iterator pos) {
+ list_.erase_after(&(*pos));
+ return ++pos;
+ }
+
+ // Removes all items from the list. The items themselves are not destructed.
void clear() { list_.clear(); }
+ // Removes this specific item from the list, if it is present. Finds the item
+ // by identity (address comparison) rather than value equality. Returns true
+ // if the item was removed; false if it was not present.
+ bool remove(const T& item) { return list_.remove(item); }
+
+ // Reference to the first element in the list. Undefined behavior if empty().
T& front() { return *static_cast<T*>(list_.begin()); }
+ // Reference to the last element in the list. Undefined behavior if empty().
+ T& back() { return *static_cast<T*>(list_.before_end()); }
+
+ // As in std::forward_list, returns the iterator before the begin() iterator.
+ iterator before_begin() noexcept {
+ return iterator(static_cast<Item*>(list_.before_begin()));
+ }
+ const_iterator before_begin() const noexcept {
+ return const_iterator(static_cast<const Item*>(list_.before_begin()));
+ }
+ const_iterator cbefore_begin() const noexcept { return before_begin(); }
+
iterator begin() noexcept {
return iterator(static_cast<Item*>(list_.begin()));
}
const_iterator begin() const noexcept {
- return const_iterator(static_cast<const Item*>(list_.cbegin()));
+ return const_iterator(static_cast<const Item*>(list_.begin()));
}
- const_iterator cbegin() const noexcept {
- return const_iterator(static_cast<const Item*>(list_.cbegin()));
- }
+ const_iterator cbegin() const noexcept { return begin(); }
- iterator end() noexcept { return iterator(); }
- const_iterator end() const noexcept { return const_iterator(); }
- const_iterator cend() const noexcept { return const_iterator(); }
+ iterator end() noexcept { return iterator(static_cast<Item*>(list_.end())); }
+ const_iterator end() const noexcept {
+ return const_iterator(static_cast<const Item*>(list_.end()));
+ }
+ const_iterator cend() const noexcept { return end(); }
private:
intrusive_list_impl::List list_;