diff --git a/src/math.hpp b/src/math.hpp
index bf964ba38c07487914ffcf5768b7f39aee63a5d1..36767bcbc20c57a581210c8a3b5ea831aafbde0b 100644
--- a/src/math.hpp
+++ b/src/math.hpp
@@ -11,7 +11,7 @@ namespace math {
 template <typename T>
 T constexpr pi()
 {
-    return T(3.1415926535897932384626433832795);
+    return T(3.1415926535897932384626433832795l);
 }
 
 template <typename T = float>
diff --git a/src/util/filter.hpp b/src/util/filter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6da37f57c175dacfd1a989c4e39a3da6544f47f8
--- /dev/null
+++ b/src/util/filter.hpp
@@ -0,0 +1,244 @@
+#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; }
+    };
+}
+
+/*
+ * Iterate through a sequence such that dereference only
+ * gives items from the range that satisfy a given predicate.
+ *
+ * Type parameters:
+ *     I      Iterator type
+ *     S      Sentinel type compatible with I
+ *     F      Functional object
+ *
+ * The underlying sequence is described by an iterator of type
+ * I and a sentinel of type S. The predicate has type F.
+ */
+
+template <typename I, typename S, typename F>
+class filter_iterator {
+    mutable I inner_;
+    S end_;
+    mutable bool ok_;
+
+    // F may be a lambda type, and thus non-copy assignable. The
+    // use of `uninitalized` allows us to work around this limitation;
+    // f_ will always be in an initalized state post-construction.
+    mutable uninitialized<F> f_;
+
+    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/src/util/transform.hpp b/src/util/transform.hpp
index ec86ea71d5c46f053711783e21795a78d3904a41..71c49e723017deb92258dc02be21e66da7dd9118 100644
--- a/src/util/transform.hpp
+++ b/src/util/transform.hpp
@@ -26,7 +26,10 @@ class transform_iterator: public iterator_adaptor<transform_iterator<I, F>, I> {
     friend class iterator_adaptor<transform_iterator<I, F>, I>;
 
     I inner_;
-    uninitialized<F> f_; // always in initialized state post-construction
+
+    // F may be a lambda type, and thus non-copy assignable. The
+    // use of `uninitalized` allows us to work around this limitation;
+    uninitialized<F> f_;
 
     // provides access to inner iterator for adaptor.
     const I& inner() const { return inner_; }
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_math.cpp b/tests/unit/test_math.cpp
index b32bd194cb75b9df1c31c34c0323fdc0b95bce9e..5dff6c81e950d901012ffe4dcce995c02a3b5cd3 100644
--- a/tests/unit/test_math.cpp
+++ b/tests/unit/test_math.cpp
@@ -6,6 +6,78 @@
 
 using namespace nest::mc::math;
 
+TEST(math, pi) {
+    // check regression against long double literal in implementation
+    auto pi_ld = pi<long double>();
+    auto pi_d = pi<double>();
+
+    if (std::numeric_limits<long double>::digits>std::numeric_limits<double>::digits) {
+        EXPECT_NE(0.0, pi_ld-pi_d);
+    }
+    else {
+        EXPECT_EQ(0.0, pi_ld-pi_d);
+    }
+
+    // library quality of implementation dependent, but expect cos(pi) to be within
+    // 1 epsilon of -1.
+
+    auto eps_d = std::numeric_limits<double>::epsilon();
+    auto cos_pi_d = std::cos(pi_d);
+    EXPECT_LE(-1.0-eps_d, cos_pi_d);
+    EXPECT_GE(-1.0+eps_d, cos_pi_d);
+
+    auto eps_ld = std::numeric_limits<long double>::epsilon();
+    auto cos_pi_ld = std::cos(pi_ld);
+    EXPECT_LE(-1.0-eps_ld, cos_pi_ld);
+    EXPECT_GE(-1.0+eps_ld, cos_pi_ld);
+}
+
+TEST(math, lerp) {
+    // expect exact computation when u is zero or one
+    double a = 1.0/3;
+    double b = 11.0/7;
+
+    EXPECT_EQ(a, lerp(a, b, 0.));
+    EXPECT_EQ(b, lerp(a, b, 1.));
+
+    // expect exact computation here as well
+    EXPECT_EQ(2.75, lerp(2.0, 3.0, 0.75));
+
+    // and otherwise to be close
+    EXPECT_DOUBLE_EQ(100.101, lerp(100.1, 200.1, 0.00001));
+    EXPECT_DOUBLE_EQ(200.099, lerp(100.1, 200.1, 0.99999));
+
+    // should be able to lerp with differing types for end points and u
+    EXPECT_EQ(0.25f, lerp(0.f, 1.f, 0.25));
+}
+
+TEST(math, frustrum) {
+    // cross check against cone calculation
+    auto cone_area = [](double l, double r) {
+        return std::hypot(l,r)*r*pi<double>();
+    };
+
+    auto cone_volume = [](double l, double r) {
+        return pi<double>()*square(r)*l/3.0;
+    };
+
+    EXPECT_DOUBLE_EQ(cone_area(5.0, 1.3), area_frustrum(5.0, 0.0, 1.3));
+    EXPECT_DOUBLE_EQ(cone_volume(5.0, 1.3), volume_frustrum(5.0, 0.0, 1.3));
+
+    double r1 = 7.0;
+    double r2 = 9.0;
+    double l = 11.0;
+
+    double s = l*r2/(r2-r1);
+    double ca = cone_area(s, r2)-cone_area(s-l, r1);
+    double cv = cone_volume(s, r2)-cone_volume(s-l, r1);
+
+    EXPECT_DOUBLE_EQ(ca, area_frustrum(l, r1, r2));
+    EXPECT_DOUBLE_EQ(ca, area_frustrum(l, r2, r1));
+    EXPECT_DOUBLE_EQ(cv, volume_frustrum(l, r1, r2));
+    EXPECT_DOUBLE_EQ(cv, volume_frustrum(l, r2, r1));
+}
+
 TEST(math, infinity) {
     // check values for float, double, long double
     auto finf = infinity<float>();
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;