pw_containers: FilteredView class
- The FilteredView class applies a filter to a container, allowing
iteration over only matching elements.
- Prevent copying an IntrusiveList::Item, since this would break the
intrusive list the item is in.
Change-Id: Ifefb95c421e057e746440532f0af6505fb2dc32d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/64960
Commit-Queue: Wyatt Hepler <hepler@google.com>
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
diff --git a/pw_containers/BUILD.bazel b/pw_containers/BUILD.bazel
index c3e9000..92c1f3f 100644
--- a/pw_containers/BUILD.bazel
+++ b/pw_containers/BUILD.bazel
@@ -53,6 +53,12 @@
)
pw_cc_library(
+ name = "filtered_view",
+ hdrs = ["public/pw_containers/filtered_view.h"],
+ includes = ["public"],
+)
+
+pw_cc_library(
name = "flat_map",
hdrs = [
"public/pw_containers/flat_map.h",
@@ -61,6 +67,15 @@
)
pw_cc_test(
+ name = "filtered_view_test",
+ srcs = ["filtered_view_test.cc"],
+ deps = [
+ ":filtered_view",
+ ":intrusive_list",
+ ],
+)
+
+pw_cc_test(
name = "flat_map_test",
srcs = [
"flat_map_test.cc",
diff --git a/pw_containers/BUILD.gn b/pw_containers/BUILD.gn
index d59ba21..82f482d 100644
--- a/pw_containers/BUILD.gn
+++ b/pw_containers/BUILD.gn
@@ -18,7 +18,7 @@
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
-config("default_config") {
+config("public_include_path") {
include_dirs = [ "public" ]
}
@@ -30,19 +30,24 @@
]
}
+pw_source_set("filtered_view") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_containers/filtered_view.h" ]
+}
+
pw_source_set("flat_map") {
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public = [ "public/pw_containers/flat_map.h" ]
}
pw_source_set("vector") {
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public_deps = [ dir_pw_assert ]
public = [ "public/pw_containers/vector.h" ]
}
pw_source_set("intrusive_list") {
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public = [
"public/pw_containers/internal/intrusive_list_impl.h",
"public/pw_containers/intrusive_list.h",
@@ -53,12 +58,21 @@
pw_test_group("tests") {
tests = [
+ ":filtered_view_test",
":flat_map_test",
":intrusive_list_test",
":vector_test",
]
}
+pw_test("filtered_view_test") {
+ sources = [ "filtered_view_test.cc" ]
+ deps = [
+ ":filtered_view",
+ ":intrusive_list",
+ ]
+}
+
pw_test("flat_map_test") {
sources = [ "flat_map_test.cc" ]
deps = [ ":flat_map" ]
diff --git a/pw_containers/docs.rst b/pw_containers/docs.rst
index 51b224b..be63a98 100644
--- a/pw_containers/docs.rst
+++ b/pw_containers/docs.rst
@@ -90,9 +90,26 @@
need to be sorted. During construction, ``pw::containers::FlatMap`` will
perform a constexpr insertion sort.
+pw::containers::FilteredView
+============================
+``pw::containers::FilteredView`` provides a view of a container that only
+contains elements that match the specified filter. This class is similar to
+C++20's `std::ranges::filter_view
+<https://en.cppreference.com/w/cpp/ranges/filter_view>`_.
+
+To create a ``FilteredView``, pass a container and a filter object, which may be
+a lambda or class that implements ``operator()`` for the container's value type.
+
+.. code-block:: cpp
+
+ std::array<int, 99> kNumbers = {3, 1, 4, 1, ...};
+
+ for (int even : FilteredView(kNumbers, [](int n) { return n % 2 == 0; })) {
+ PW_LOG_INFO("This number is even: %d", even);
+ }
+
Compatibility
=============
-* C
* C++17
Dependencies
diff --git a/pw_containers/filtered_view_test.cc b/pw_containers/filtered_view_test.cc
new file mode 100644
index 0000000..1583981
--- /dev/null
+++ b/pw_containers/filtered_view_test.cc
@@ -0,0 +1,176 @@
+// 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
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_containers/filtered_view.h"
+
+#include <array>
+#include <span>
+
+#include "gtest/gtest.h"
+#include "pw_containers/intrusive_list.h"
+
+namespace pw::containers {
+namespace {
+
+struct Item : IntrusiveList<Item>::Item {
+ constexpr Item(int x) : value(x) {}
+
+ int value;
+};
+
+constexpr std::array<int, 6> kArray{0, 1, 2, 3, 4, 5};
+
+TEST(FilteredView, Array_MatchSubset) {
+ FilteredView view(kArray, [](int x) { return x == 3 || x == 5; });
+
+ auto it = view.begin();
+ ASSERT_EQ(*it, 3);
+ ++it;
+ ASSERT_EQ(*it, 5);
+ ++it;
+ EXPECT_EQ(it, view.end());
+}
+
+TEST(FilteredView, Array_MatchAll) {
+ FilteredView view(kArray, [](int) { return true; });
+
+ std::array<bool, 6> found = {};
+ for (int value : view) {
+ found[value] = true;
+ }
+ EXPECT_TRUE(
+ std::all_of(found.begin(), found.end(), [](bool b) { return b; }));
+}
+
+TEST(FilteredView, Array_MatchNone) {
+ for (int unused : FilteredView(kArray, [](int) { return false; })) {
+ static_cast<void>(unused);
+ FAIL();
+ }
+}
+
+TEST(FilteredView, EmptyContainer) {
+ constexpr std::array<int, 0> nothing{};
+ for (int unused : FilteredView(nothing, [](int) { return true; })) {
+ static_cast<void>(unused);
+ FAIL();
+ }
+
+ IntrusiveList<Item> intrusive_list;
+ for (const Item& unused :
+ FilteredView(nothing, [](const Item&) { return true; })) {
+ static_cast<void>(unused);
+ FAIL();
+ }
+}
+
+TEST(FilteredView, IntrusiveList_MatchSubset) {
+ Item item_1{1};
+ Item item_2{2};
+ Item item_3{3};
+ IntrusiveList<Item> intrusive_list({&item_1, &item_2, &item_3});
+
+ FilteredView view(intrusive_list,
+ [](const Item& i) { return i.value % 2 != 0; });
+
+ auto it = view.begin();
+ ASSERT_EQ(it->value, 1);
+ ++it;
+ ASSERT_EQ((*it).value, 3);
+ ++it;
+ EXPECT_EQ(it, view.end());
+}
+
+TEST(FilteredView, IntrusiveList_MatchAll) {
+ Item item_1{0};
+ Item item_2{1};
+ Item item_3{2};
+ IntrusiveList<Item> intrusive_list({&item_1, &item_2, &item_3});
+
+ std::array<bool, 3> found = {};
+
+ for (const Item& item :
+ FilteredView(intrusive_list, [](const Item&) { return true; })) {
+ found[item.value] = true;
+ }
+ EXPECT_TRUE(
+ std::all_of(found.begin(), found.end(), [](bool b) { return b; }));
+}
+
+TEST(FilteredView, IntrusiveList_MatchNone) {
+ Item item_1{0};
+ Item item_2{1};
+ Item item_3{2};
+ IntrusiveList<Item> intrusive_list({&item_1, &item_2, &item_3});
+
+ for (const Item& unused :
+ FilteredView(kArray, [](const Item&) { return false; })) {
+ static_cast<void>(unused);
+ FAIL();
+ }
+}
+
+TEST(FilteredView, Front_OneElement) {
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x == 0; }).front(), 0);
+}
+
+TEST(FilteredView, Back_OneElement) {
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x == 0; }).back(), 0);
+}
+
+TEST(FilteredView, Front_MultipleElements) {
+ EXPECT_EQ(
+ FilteredView(kArray, [](int x) { return x == 3 || x == 5; }).front(), 3);
+}
+
+TEST(FilteredView, Back_MultipleElements) {
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x == 3 || x == 5; }).back(),
+ 5);
+}
+
+TEST(FilteredView, Size_Empty) {
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x < 0; }).size(), 0u);
+
+ EXPECT_TRUE(FilteredView(kArray, [](int x) { return x < 0; }).empty());
+
+ constexpr std::array<int, 0> empty{};
+ FilteredView empty_view(empty, [](const Item&) { return true; });
+ EXPECT_EQ(empty_view.size(), 0u);
+ EXPECT_TRUE(empty_view.empty());
+}
+
+TEST(FilteredView, Size_OneElement) {
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x == 0; }).size(), 1u);
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x == 3; }).size(), 1u);
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x == 5; }).size(), 1u);
+
+ EXPECT_FALSE(FilteredView(kArray, [](int x) { return x == 5; }).empty());
+}
+
+TEST(FilteredView, Size_MultipleElements) {
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x <= 1; }).size(), 2u);
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x > 1; }).size(), 4u);
+ EXPECT_EQ(FilteredView(kArray, [](int x) { return x < 5; }).size(), 5u);
+
+ EXPECT_FALSE(FilteredView(kArray, [](int x) { return x < 5; }).empty());
+}
+
+TEST(FilteredView, Size_AllElements) {
+ EXPECT_EQ(FilteredView(kArray, [](int) { return true; }).size(), 6u);
+
+ EXPECT_FALSE(FilteredView(kArray, [](int x) { return x < 5; }).empty());
+}
+
+} // namespace
+} // namespace pw::containers
diff --git a/pw_containers/public/pw_containers/filtered_view.h b/pw_containers/public/pw_containers/filtered_view.h
new file mode 100644
index 0000000..7daf821
--- /dev/null
+++ b/pw_containers/public/pw_containers/filtered_view.h
@@ -0,0 +1,165 @@
+// 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
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <iterator>
+
+#include "pw_assert/assert.h"
+
+namespace pw::containers {
+
+// FilteredView supports iterating over only elements that match a filter in a
+// container. FilteredView works with any container with an incrementable
+// iterator. The back() function currently requires a bidirectional iterator.
+//
+// FilteredView is similar to C++20's std::filter_view.
+template <typename Container, typename Filter>
+class FilteredView {
+ public:
+ // Iterator that only moves to elements that match the provided filter.
+ class iterator {
+ public:
+ using difference_type = std::ptrdiff_t;
+ using value_type = typename Container::value_type;
+ using pointer = typename Container::pointer;
+ using reference = typename Container::reference;
+ using iterator_category = std::bidirectional_iterator_tag;
+
+ constexpr iterator() : view_(nullptr), it_(0) {}
+
+ iterator& operator++();
+
+ iterator operator++(int) {
+ iterator original = *this;
+ operator++();
+ return original;
+ }
+
+ iterator& operator--();
+
+ iterator operator--(int) {
+ iterator original = *this;
+ operator--();
+ return original;
+ }
+
+ const auto& operator*() const { return value(); }
+
+ const auto* operator->() const { return &value(); }
+
+ constexpr bool operator==(const iterator& other) const {
+ return view_ == other.view_ && it_ == other.it_;
+ }
+
+ constexpr bool operator!=(const iterator& other) const {
+ return !(*this == other);
+ }
+
+ private:
+ friend class FilteredView;
+
+ enum EndIterator { kEnd };
+
+ explicit iterator(const FilteredView& view)
+ : view_(&view), it_(view.container_.begin()) {
+ FindMatch();
+ }
+
+ iterator(const FilteredView& view, EndIterator)
+ : view_(&view), it_(view.container_.end()) {}
+
+ // Accesses the value referred to by this iterator.
+ const auto& value() const { return *it_; }
+
+ // Iterates until a match is found, up to end().
+ void FindMatch();
+
+ bool MatchesItem(const value_type& value) const {
+ return view_->filter_(value);
+ }
+
+ const FilteredView* view_;
+ typename Container::const_iterator it_;
+ };
+
+ using const_iterator = iterator;
+
+ template <typename... FilterArgs>
+ constexpr FilteredView(const Container& container, Filter&& filter)
+ : container_(container), filter_(std::move(filter)) {}
+
+ constexpr FilteredView(const FilteredView&) = delete;
+ constexpr FilteredView& operator=(const FilteredView&) = delete;
+
+ const auto& operator[](size_t index) const {
+ auto it = begin();
+ std::advance(it, index);
+ return *it;
+ }
+
+ // Accesses the first matching element. Invalid if empty().
+ const auto& front() const { return *begin(); }
+
+ // Accesses the last matching element. Invalid if empty().
+ const auto& back() const { return *std::prev(end()); }
+
+ // The number of elements in the container that match the filter.
+ size_t size() const { return std::distance(begin(), end()); }
+
+ bool empty() const { return begin() == end(); }
+
+ iterator begin() const { return iterator(*this); }
+ iterator end() const { return iterator(*this, iterator::kEnd); }
+
+ private:
+ const Container& container_;
+ Filter filter_;
+};
+
+template <typename Container, typename Filter>
+void FilteredView<Container, Filter>::iterator::FindMatch() {
+ for (; it_ != view_->container_.end(); ++it_) {
+ if (MatchesItem(*it_)) {
+ break;
+ }
+ }
+}
+
+template <typename Container, typename Filter>
+typename FilteredView<Container, Filter>::iterator&
+FilteredView<Container, Filter>::iterator::operator++() {
+ PW_ASSERT(it_ != view_->container_.end());
+
+ ++it_;
+ FindMatch();
+ return *this;
+}
+
+template <typename Container, typename Filter>
+typename FilteredView<Container, Filter>::iterator&
+FilteredView<Container, Filter>::iterator::operator--() {
+ decltype(it_) new_it = view_->container_.end();
+ while (new_it != view_->container_.begin()) {
+ --new_it;
+ if (MatchesItem(*new_it)) {
+ it_ = new_it;
+ return *this;
+ }
+ }
+
+ PW_ASSERT(false);
+}
+
+} // namespace pw::containers
diff --git a/pw_containers/public/pw_containers/intrusive_list.h b/pw_containers/public/pw_containers/intrusive_list.h
index b7e800f..9232203 100644
--- a/pw_containers/public/pw_containers/intrusive_list.h
+++ b/pw_containers/public/pw_containers/intrusive_list.h
@@ -53,6 +53,10 @@
class IntrusiveList {
public:
class Item : public intrusive_list_impl::List::Item {
+ public:
+ Item(const Item&) = delete;
+ Item& operator=(const Item&) = delete;
+
protected:
constexpr Item() = default;