diff --git a/src/util/filter.hpp b/src/util/filter.hpp new file mode 100644 index 0000000000000000000000000000000000000000..5fcbd1fd687e9a191f168036fb5925c961743614 --- /dev/null +++ b/src/util/filter.hpp @@ -0,0 +1,227 @@ +#pragma once + +/* + * An iterator adaptor that lazily skips items not matching a predicate. + */ + +#include <iterator> +#include <memory> +#include <type_traits> + +#include <util/iterutil.hpp> +#include <util/meta.hpp> +#include <util/range.hpp> + +#include <util/debug.hpp> + +namespace nest { +namespace mc { +namespace util { + +namespace impl { + template <typename I, bool = std::is_pointer<I>::value> + struct arrow { + using type = decltype(std::declval<I>().operator->()); + static type eval(I& x) { return x.operator->(); } + }; + + template <typename I> + struct arrow<I, true> { + using type = I; + static type eval(I& x) { return x; } + }; +} + +template <typename I, typename S, typename F> +class filter_iterator { + mutable I inner_; + S end_; + mutable bool ok_; + mutable uninitialized<F> f_; // always in initialized state post-construction + + void advance() const { + if (ok_) return; + + for (;;) { + ok_ = inner_==end_ || f_.ref()(*inner_); + if (ok_) break; + ++inner_; + } + } + +public: + using value_type = typename std::iterator_traits<I>::value_type; + using difference_type = typename std::iterator_traits<I>::difference_type; + using iterator_category = typename std::conditional< + is_forward_iterator<I>::value, + std::forward_iterator_tag, + std::input_iterator_tag + >::type; + + using pointer = typename std::iterator_traits<I>::pointer; + using reference = typename std::iterator_traits<I>::reference; + + filter_iterator(): ok_{inner_==end_} {} + + template <typename J, typename K, typename G> + filter_iterator(J&& iter, K&& end, G&& f): + inner_(std::forward<J>(iter)), + end_(std::forward<K>(end)), + ok_{inner_==end_} + { + f_.construct(std::forward<G>(f)); + } + + filter_iterator(const filter_iterator& other): + inner_(other.inner_), + end_(other.end_), + ok_{other.ok_} + { + f_.construct(other.f_.cref()); + } + + filter_iterator(filter_iterator&& other): + inner_(std::move(other.inner_)), + end_(std::move(other.end_)), + ok_{other.ok_} + { + f_.construct(std::move(other.f_.ref())); + } + + filter_iterator& operator=(filter_iterator&& other) { + if (this!=&other) { + inner_ = std::move(other.inner_); + end_ = std::move(other.end_); + ok_ = other.ok_; + f_.construct(std::move(other.f_.ref())); + } + return *this; + } + + filter_iterator& operator=(const filter_iterator& other) { + if (this!=&other) { + inner_ = other.inner_; + end_ = other.end_; + ok_ = other.ok_; + f_.destruct(); + f_.construct(other.f_.cref()); + } + return *this; + } + + // forward and input iterator requirements + + auto operator*() const -> decltype(*(this->inner_)) { + advance(); + return *inner_; + } + + typename impl::arrow<I>::type + operator->() const { + advance(); + return impl::arrow<I>::eval(inner_); + } + + filter_iterator& operator++() { + advance(); + ok_ = false; + ++inner_; + return *this; + } + + filter_iterator operator++(int) { + auto c(*this); + ++*this; + advance(); + return c; + } + + bool operator==(const filter_iterator& other) const { + advance(); + other.advance(); + return inner_==other.inner_; + } + + bool operator!=(const filter_iterator& other) const { + return !(*this==other); + } + + // expose inner iterator for testing against a sentinel + template <typename Sentinel> + bool operator==(const Sentinel& s) const { + advance(); + return inner_==s; + } + + template <typename Sentinel> + bool operator!=(const Sentinel& s) const { return !(inner_==s); } + + // public access to inner iterator + const I& get() const { + advance(); + return inner_; + } +}; + +template <typename I, typename S, typename F> +filter_iterator<I, S, util::decay_t<F>> make_filter_iterator(const I& i, const S& end, const F& f) { + return filter_iterator<I, S, util::decay_t<F>>(i, end, f); +} + +// filter over const and non-const regular sequences: + +template < + typename Seq, + typename F, + typename seq_iter = typename sequence_traits<Seq>::iterator, + typename seq_sent = typename sequence_traits<Seq>::sentinel, + typename = enable_if_t<std::is_same<seq_iter, seq_sent>::value> +> +range<filter_iterator<seq_iter, seq_iter, util::decay_t<F>>> +filter(Seq& s, const F& f) { + return {make_filter_iterator(std::begin(s), std::end(s), f), + make_filter_iterator(std::end(s), std::end(s), f)}; +} + +template < + typename Seq, + typename F, + typename seq_citer = typename sequence_traits<Seq>::const_iterator, + typename seq_csent = typename sequence_traits<Seq>::const_sentinel, + typename = enable_if_t<std::is_same<seq_citer, seq_csent>::value> +> +range<filter_iterator<seq_citer, seq_citer, util::decay_t<F>>> +filter(const Seq& s, const F& f) { + return {make_filter_iterator(cbegin(s), cend(s), f), + make_filter_iterator(cend(s), cend(s), f)}; +} + +// filter over const and non-const sentinel-terminated sequences: + +template < + typename Seq, + typename F, + typename seq_iter = typename sequence_traits<Seq>::iterator, + typename seq_sent = typename sequence_traits<Seq>::sentinel, + typename = enable_if_t<!std::is_same<seq_iter, seq_sent>::value> +> +range<filter_iterator<seq_iter, seq_sent, util::decay_t<F>>, seq_sent> +filter(Seq& s, const F& f) { + return {make_filter_iterator(std::begin(s), std::end(s), f), std::end(s)}; +} + +template < + typename Seq, + typename F, + typename seq_citer = typename sequence_traits<Seq>::const_iterator, + typename seq_csent = typename sequence_traits<Seq>::const_sentinel, + typename = enable_if_t<!std::is_same<seq_citer, seq_csent>::value> +> +range<filter_iterator<seq_citer, seq_csent, util::decay_t<F>>, seq_csent> +filter(const Seq& s, const F& f) { + return {make_filter_iterator(cbegin(s), cend(s), f), cend(s)}; +} + +} // namespace util +} // namespace mc +} // namespace nest diff --git a/src/util/iterutil.hpp b/src/util/iterutil.hpp index 685398579a14c32971911087d772182d17b3d12d..8c327d4140c24790ca7576fafb190932508524e6 100644 --- a/src/util/iterutil.hpp +++ b/src/util/iterutil.hpp @@ -67,6 +67,20 @@ distance(I first, E last) { return ret; } +/* + * generic front() and back() methods for containers or ranges + */ + +template <typename Seq> +auto front(Seq& seq) -> decltype(*std::begin(seq)) { + return *std::begin(seq); +} + +template <typename Seq> +auto back(Seq& seq) -> decltype(*std::begin(seq)) { + return *upto(std::begin(seq), std::end(seq)); +} + /* * Provide a proxy object for operator->() for iterator adaptors that * present rvalues on dereference. diff --git a/src/util/rangeutil.hpp b/src/util/rangeutil.hpp index 839fb4527be34cf04777c65d7e934712aa7c57e3..bc47dec20767f05663283812ecd12caed831eb27 100644 --- a/src/util/rangeutil.hpp +++ b/src/util/rangeutil.hpp @@ -120,6 +120,47 @@ sort_by(const Seq& seq, const Proj& proj) { }); } +// Stable sort in-place by projection `proj` + +template <typename Seq, typename Proj> +enable_if_t<!std::is_const<typename sequence_traits<Seq>::reference>::value> +stable_sort_by(Seq& seq, const Proj& proj) { + using value_type = typename sequence_traits<Seq>::value_type; + auto canon = canonical_view(seq); + + std::stable_sort(std::begin(canon), std::end(canon), + [&proj](const value_type& a, const value_type& b) { + return proj(a) < proj(b); + }); +} + +template <typename Seq, typename Proj> +enable_if_t<!std::is_const<typename sequence_traits<Seq>::reference>::value> +stable_sort_by(const Seq& seq, const Proj& proj) { + using value_type = typename sequence_traits<Seq>::value_type; + auto canon = canonical_view(seq); + + std::stable_sort(std::begin(canon), std::end(canon), + [&proj](const value_type& a, const value_type& b) { + return proj(a) < proj(b); + }); +} + +// Range-interface for `all_of`, `any_of` + +template <typename Seq, typename Predicate> +bool all_of(const Seq& seq, const Predicate& pred) { + auto canon = canonical_view(seq); + return std::all_of(std::begin(canon), std::end(canon), pred); +} + +template <typename Seq, typename Predicate> +bool any_of(const Seq& seq, const Predicate& pred) { + auto canon = canonical_view(seq); + return std::any_of(std::begin(canon), std::end(canon), pred); +} + + // Accumulate by projection `proj` template < diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 60ca6ab0572c74a580399364609d16b9c242adaa..f72a08b14359305e3eab73b220f90fece310eb68 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -7,6 +7,7 @@ set(TEST_SOURCES test_counter.cpp test_either.cpp test_event_queue.cpp + test_filter.cpp test_fvm.cpp test_fvm_multi.cpp test_cell_group.cpp diff --git a/tests/unit/common.hpp b/tests/unit/common.hpp new file mode 100644 index 0000000000000000000000000000000000000000..687bfef82e4f7ac895e17fdce17b0a5e64f77d38 --- /dev/null +++ b/tests/unit/common.hpp @@ -0,0 +1,109 @@ +#pragma once + +/* + * Convenience functions, structs used across + * more than one unit test. + */ + +namespace testing { + +// sentinel for use with range-related tests + +struct null_terminated_t { + bool operator==(const char *p) const { return !*p; } + bool operator!=(const char *p) const { return !!*p; } + + friend bool operator==(const char *p, null_terminated_t x) { + return x==p; + } + + friend bool operator!=(const char *p, null_terminated_t x) { + return x!=p; + } + + constexpr null_terminated_t() {} +}; + +constexpr null_terminated_t null_terminated; + +// wrap a value type, with copy operations disabled + +template <typename V> +struct nocopy { + V value; + + nocopy(): value{} {} + nocopy(V v): value(v) {} + nocopy(const nocopy& n) = delete; + + nocopy(nocopy&& n) { + value=n.value; + n.value=V{}; + ++move_ctor_count; + } + + nocopy& operator=(const nocopy& n) = delete; + nocopy& operator=(nocopy&& n) { + value=n.value; + n.value=V{}; + ++move_assign_count; + return *this; + } + + bool operator==(const nocopy& them) const { return them.value==value; } + bool operator!=(const nocopy& them) const { return !(*this==them); } + + static int move_ctor_count; + static int move_assign_count; + static void reset_counts() { + move_ctor_count = 0; + move_assign_count = 0; + } +}; + +template <typename V> +int nocopy<V>::move_ctor_count; + +template <typename V> +int nocopy<V>::move_assign_count; + +// wrap a value type, with move operations disabled + +template <typename V> +struct nomove { + V value; + + nomove(): value{} {} + nomove(V v): value(v) {} + nomove(nomove&& n) = delete; + + nomove(const nomove& n): value(n.value) { + ++copy_ctor_count; + } + + nomove& operator=(nomove&& n) = delete; + + nomove& operator=(const nomove& n) { + value=n.value; + ++copy_assign_count; + return *this; + } + + bool operator==(const nomove& them) const { return them.value==value; } + bool operator!=(const nomove& them) const { return !(*this==them); } + + static int copy_ctor_count; + static int copy_assign_count; + static void reset_counts() { + copy_ctor_count = 0; + copy_assign_count = 0; + } +}; + +template <typename V> +int nomove<V>::copy_ctor_count; + +template <typename V> +int nomove<V>::copy_assign_count; + +} // namespace testing diff --git a/tests/unit/test_filter.cpp b/tests/unit/test_filter.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0d357edd3c53fc62c1618f4173935bec27179af6 --- /dev/null +++ b/tests/unit/test_filter.cpp @@ -0,0 +1,96 @@ +#include "gtest.h" + +#include <cstring> +#include <list> + +#include <util/range.hpp> +#include <util/rangeutil.hpp> +#include <util/filter.hpp> +#include <util/transform.hpp> + +#include "common.hpp" + +using namespace nest::mc; +using util::filter; +using util::assign; +using util::canonical_view; + +util::range<const char*, testing::null_terminated_t> +cstring(const char* s) { + return {s, testing::null_terminated}; +} + +util::range<char*, testing::null_terminated_t> +cstring(char* s) { + return {s, testing::null_terminated}; +} + +TEST(filter, const_regular) { + std::list<int> ten = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + std::list<int> odd = {1, 3, 5, 7, 9}; + std::list<int> even = {2, 4, 6, 8, 10}; + std::list<int> check; + + assign(check, filter(ten, [](int i) { return i%2!=0; })); + EXPECT_EQ(odd, check); + + assign(check, filter(ten, [](int i) { return i%2==0; })); + EXPECT_EQ(even, check); +} + +TEST(filter, const_sentinel) { + auto xyzzy = cstring("xyzzy"); + std::string check; + + assign(check, filter(xyzzy, [](char c) { return c!='y'; })); + EXPECT_EQ(check, "xzz"); + + assign(check, filter(xyzzy, [](char c) { return c!='x'; })); + EXPECT_EQ(check, "yzzy"); +} + +TEST(filter, modify_regular) { + int nums[] = {1, -2, 3, -4, -5}; + + for (auto& n: filter(nums, [](int i) { return i<0; })) { + n *= n; + } + + int check[] = {1, 4, 3, 16, 25}; + for (unsigned i = 0; i<5; ++i) { + EXPECT_EQ(check[i], nums[i]); + } +} + +TEST(filter, modify_sentinel) { + char xoxo[] = "xoxo"; + for (auto& c: canonical_view(filter(cstring(xoxo), [](char c) { return c=='o'; }))) { + c = 'q'; + } + + EXPECT_TRUE(!std::strcmp(xoxo, "xqxq")); +} + + +TEST(filter, laziness) { + std::list<int> ten = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + int derefs = 0; + auto count_derefs = [&derefs](int i) { ++derefs; return i; }; + + auto odd = filter(util::transform_view(ten, count_derefs), + [](int i) { return i%2!=0; }); + + auto iter = std::begin(odd); + + EXPECT_EQ(0, derefs); + + EXPECT_EQ(1, *iter); + EXPECT_EQ(2, derefs); // one for check, one for operator*() + ++iter; // does not need to scan ahead + EXPECT_EQ(2, derefs); + + ++iter; // now it does need to scan ahead, testing values 2 and 3. + EXPECT_EQ(4, derefs); + EXPECT_EQ(5, *iter); +} diff --git a/tests/unit/test_optional.cpp b/tests/unit/test_optional.cpp index fe34cc3dc1278f02b866aefc3c0b8942a1169d98..4ada9ca81730e28aa612b2899e94eebbcfdb32aa 100644 --- a/tests/unit/test_optional.cpp +++ b/tests/unit/test_optional.cpp @@ -4,6 +4,7 @@ #include "gtest.h" #include "util/optional.hpp" +#include "common.hpp" using namespace nest::mc::util; @@ -128,21 +129,9 @@ TEST(optionalm,assign_reference) { EXPECT_EQ(&br, &check_rval2); } -struct nomove { - int value; - - nomove(): value(0) {} - nomove(int i): value(i) {} - nomove(const nomove& n): value(n.value) {} - nomove(nomove&& n) = delete; - - nomove& operator=(const nomove& n) { value=n.value; return *this; } - - bool operator==(const nomove& them) const { return them.value==value; } - bool operator!=(const nomove& them) const { return !(*this==them); } -}; - TEST(optionalm,ctor_nomove) { + using nomove = testing::nomove<int>; + optional<nomove> a(nomove(3)); EXPECT_EQ(nomove(3),a.get()); @@ -154,38 +143,25 @@ TEST(optionalm,ctor_nomove) { EXPECT_EQ(nomove(4),b.get()); } -struct nocopy { - int value; - - nocopy(): value(0) {} - nocopy(int i): value(i) {} - nocopy(const nocopy& n) = delete; - nocopy(nocopy&& n) { - value=n.value; - n.value=0; - } - - nocopy& operator=(const nocopy& n) = delete; - nocopy& operator=(nocopy&& n) { - value=n.value; - n.value=-1; - return *this; - } - - bool operator==(const nocopy& them) const { return them.value==value; } - bool operator!=(const nocopy& them) const { return !(*this==them); } -}; - TEST(optionalm,ctor_nocopy) { + using nocopy = testing::nocopy<int>; + optional<nocopy> a(nocopy(5)); EXPECT_EQ(nocopy(5),a.get()); + nocopy::reset_counts(); optional<nocopy> b(std::move(a)); EXPECT_EQ(nocopy(5),b.get()); EXPECT_EQ(0,a.get().value); + EXPECT_EQ(1, nocopy::move_ctor_count); + EXPECT_EQ(0, nocopy::move_assign_count); + nocopy::reset_counts(); b=optional<nocopy>(nocopy(6)); EXPECT_EQ(nocopy(6),b.get()); + EXPECT_EQ(1, nocopy::move_ctor_count); + EXPECT_EQ(1, nocopy::move_assign_count); + } static optional<double> odd_half(int n) { diff --git a/tests/unit/test_range.cpp b/tests/unit/test_range.cpp index f486d2239c80f183a73f949ee624657902f14a75..2acf141253e42c01cf57f3c8798b4aa3eb741d3a 100644 --- a/tests/unit/test_range.cpp +++ b/tests/unit/test_range.cpp @@ -18,7 +18,10 @@ #include <util/sentinel.hpp> #include <util/transform.hpp> +#include "common.hpp" + using namespace nest::mc; +using testing::null_terminated; TEST(range, list_iterator) { std::list<int> l = { 2, 4, 6, 8, 10 }; @@ -143,23 +146,6 @@ TEST(range, const_iterator) { EXPECT_TRUE((std::is_same<const int&, decltype(r_const.front())>::value)); } -struct null_terminated_t { - bool operator==(const char *p) const { return !*p; } - bool operator!=(const char *p) const { return !!*p; } - - friend bool operator==(const char *p, null_terminated_t x) { - return x==p; - } - - friend bool operator!=(const char *p, null_terminated_t x) { - return x!=p; - } - - constexpr null_terminated_t() {} -}; - -constexpr null_terminated_t null_terminated; - TEST(range, sentinel) { const char *cstr = "hello world"; std::string s; @@ -345,6 +331,17 @@ TEST(range, sort) { // reverse sort by transform c to -c util::sort_by(util::strict_view(cstr_range), [](char c) { return -c; }); EXPECT_EQ(std::string("ywohd"), cstr); + + // stable sort: move capitals to front, numbers to back + auto rank = [](char c) { + return std::isupper(c)? 0: std::isdigit(c)? 2: 1; + }; + + char mixed[] = "t5hH4E3erLL2e1O"; + auto mixed_range = util::make_range(std::begin(mixed), null_terminated); + + util::stable_sort_by(util::strict_view(mixed_range), rank); + EXPECT_EQ(std::string("HELLOthere54321"), mixed); } TEST(range, sum_by) { @@ -361,6 +358,37 @@ TEST(range, sum_by) { EXPECT_EQ(10u, count); } +TEST(range, all_of_any_of) { + // make a C string into a sentinel-terminated range + auto cstr = [](const char* s) { return util::make_range(s, null_terminated); }; + + // predicate throws on finding 'x' in order to check + // early stop criterion. + auto pred = [](char c) { return c=='x'? throw c:c<'5'; }; + + // all + EXPECT_TRUE(util::all_of(std::string(), pred)); + EXPECT_TRUE(util::all_of(std::string("1234"), pred)); + EXPECT_FALSE(util::all_of(std::string("12345"), pred)); + EXPECT_FALSE(util::all_of(std::string("12345x"), pred)); + + EXPECT_TRUE(util::all_of(cstr(""), pred)); + EXPECT_TRUE(util::all_of(cstr("1234"), pred)); + EXPECT_FALSE(util::all_of(cstr("12345"), pred)); + EXPECT_FALSE(util::all_of(cstr("12345x"), pred)); + + // any + EXPECT_FALSE(util::any_of(std::string(), pred)); + EXPECT_FALSE(util::any_of(std::string("8765"), pred)); + EXPECT_TRUE(util::any_of(std::string("87654"), pred)); + EXPECT_TRUE(util::any_of(std::string("87654x"), pred)); + + EXPECT_FALSE(util::any_of(cstr(""), pred)); + EXPECT_FALSE(util::any_of(cstr("8765"), pred)); + EXPECT_TRUE(util::any_of(cstr("87654"), pred)); + EXPECT_TRUE(util::any_of(cstr("87654x"), pred)); +} + #ifdef WITH_TBB TEST(range, tbb_split) { diff --git a/tests/unit/test_uninitialized.cpp b/tests/unit/test_uninitialized.cpp index 1f04510d7df6af054eaec854d7e0c954ad5e73c8..718eaface74da5e7ac27812bc6292a64b538af2d 100644 --- a/tests/unit/test_uninitialized.cpp +++ b/tests/unit/test_uninitialized.cpp @@ -1,6 +1,7 @@ #include "gtest.h" #include "util/uninitialized.hpp" +#include "common.hpp" using namespace nest::mc::util; @@ -50,24 +51,8 @@ TEST(uninitialized,ctor) { EXPECT_EQ(1,count_ops::move_assign_count); } -namespace { - struct nocopy { - nocopy() {} - nocopy(const nocopy& n) = delete; - nocopy(nocopy&& n) { ++move_ctor_count; } - - nocopy& operator=(const nocopy& n) = delete; - nocopy& operator=(nocopy&& n) { ++move_assign_count; return *this; } - - static int move_ctor_count,move_assign_count; - static void reset_counts() { move_ctor_count=move_assign_count=0; } - }; - - int nocopy::move_ctor_count=0; - int nocopy::move_assign_count=0; -} - TEST(uninitialized,ctor_nocopy) { + using nocopy = testing::nocopy<int>; nocopy::reset_counts(); uninitialized<nocopy> ua; @@ -82,24 +67,8 @@ TEST(uninitialized,ctor_nocopy) { EXPECT_EQ(1,nocopy::move_assign_count); } -namespace { - struct nomove { - nomove() {} - nomove(const nomove& n) { ++copy_ctor_count; } - nomove(nomove&& n) = delete; - - nomove& operator=(const nomove& n) { ++copy_assign_count; return *this; } - nomove& operator=(nomove&& n) = delete; - - static int copy_ctor_count,copy_assign_count; - static void reset_counts() { copy_ctor_count=copy_assign_count=0; } - }; - - int nomove::copy_ctor_count=0; - int nomove::copy_assign_count=0; -} - TEST(uninitialized,ctor_nomove) { + using nomove = testing::nomove<int>; nomove::reset_counts(); uninitialized<nomove> ua;