From 74e911e6b61c4b9689095668fbdd030bb46ab60e Mon Sep 17 00:00:00 2001
From: Sam Yates <halfflat@gmail.com>
Date: Mon, 14 Sep 2020 21:46:39 +0200
Subject: [PATCH] Replace `util::either` with `util::expected`. (#1142)

* Implement a workalike for the proposed `std::expected` class: see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0323r9.html .
* Replace use of `either` with `expected` in `mprovider`, `mechanism_catalogue`, `util::partition_range`, and `pyarb::hopefully`.
* Replace use of `either` with `variant` in `util::sentinel_iterator`.
* Add `in_place_t` constructor for `util::optional`.
* Fix move assignment bug in `util::variant`.
* Remove `util/either.hpp` and associated tests.

Fixes #1135.
---
 arbor/include/arbor/morph/mprovider.hpp |   6 +-
 arbor/include/arbor/util/either.hpp     | 384 -----------------
 arbor/include/arbor/util/expected.hpp   | 524 ++++++++++++++++++++++++
 arbor/include/arbor/util/optional.hpp   |  15 +-
 arbor/include/arbor/util/variant.hpp    |   6 +-
 arbor/mechcat.cpp                       | 104 ++---
 arbor/morph/mprovider.cpp               |  15 +-
 arbor/util/filter.hpp                   |   8 +-
 arbor/util/partition.hpp                |  12 +-
 arbor/util/sentinel.hpp                 |  12 +-
 arbor/util/transform.hpp                |   8 +-
 python/error.hpp                        |  18 +-
 python/s_expr.hpp                       |   1 -
 test/unit/CMakeLists.txt                |   2 +-
 test/unit/test_either.cpp               |  65 ---
 test/unit/test_expected.cpp             | 348 ++++++++++++++++
 16 files changed, 972 insertions(+), 556 deletions(-)
 delete mode 100644 arbor/include/arbor/util/either.hpp
 create mode 100644 arbor/include/arbor/util/expected.hpp
 delete mode 100644 test/unit/test_either.cpp
 create mode 100644 test/unit/test_expected.cpp

diff --git a/arbor/include/arbor/morph/mprovider.hpp b/arbor/include/arbor/morph/mprovider.hpp
index 0169df17..7cc3e126 100644
--- a/arbor/include/arbor/morph/mprovider.hpp
+++ b/arbor/include/arbor/morph/mprovider.hpp
@@ -6,7 +6,7 @@
 #include <arbor/morph/embed_pwlin.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/label_dict.hpp>
-#include <arbor/util/either.hpp>
+#include <arbor/util/expected.hpp>
 
 namespace arb {
 
@@ -34,8 +34,8 @@ private:
     struct circular_def {};
 
     // Maps are mutated only during initialization phase of mprovider.
-    mutable std::unordered_map<std::string, util::either<mextent, circular_def>> regions_;
-    mutable std::unordered_map<std::string, util::either<mlocation_list, circular_def>> locsets_;
+    mutable std::unordered_map<std::string, util::expected<mextent, circular_def>> regions_;
+    mutable std::unordered_map<std::string, util::expected<mlocation_list, circular_def>> locsets_;
 
     // Non-null only during initialization phase.
     mutable const label_dict* label_dict_ptr;
diff --git a/arbor/include/arbor/util/either.hpp b/arbor/include/arbor/util/either.hpp
deleted file mode 100644
index 08cc6612..00000000
--- a/arbor/include/arbor/util/either.hpp
+++ /dev/null
@@ -1,384 +0,0 @@
-#pragma once
-
-/*
- * A type-safe discriminated union of two members.
- *
- * Returns true in a bool context if the first of the two types holds a value.
- */
-
-#include <cstdlib>
-#include <type_traits>
-#include <stdexcept>
-#include <utility>
-
-#include <arbor/util/uninitialized.hpp>
-
-namespace arb {
-namespace util {
-
-struct either_invalid_access: std::runtime_error {
-    explicit either_invalid_access(const std::string& what_str)
-        : std::runtime_error(what_str)
-    {}
-
-    either_invalid_access()
-        : std::runtime_error("access of unconstructed value in either")
-    {}
-};
-
-namespace detail {
-    template <typename A, typename B>
-    struct either_data {
-        union {
-            uninitialized<A> ua;
-            uninitialized<B> ub;
-        };
-
-        either_data() = default;
-
-        either_data(const either_data&) = delete;
-        either_data(either_data&&) = delete;
-        either_data& operator=(const either_data&) = delete;
-        either_data& operator=(either_data&&) = delete;
-    };
-
-    template <std::size_t, typename A, typename B> struct either_select;
-
-    template <typename A, typename B>
-    struct either_select<0, A, B> {
-        using type = uninitialized<A>;
-        static type& field(either_data<A, B>& data) { return data.ua; }
-        static const type& field(const either_data<A, B>& data) { return data.ua; }
-    };
-
-    template <typename A, typename B>
-    struct either_select<1, A, B> {
-        using type = uninitialized<B>;
-        static type& field(either_data<A, B>& data) { return data.ub; }
-        static const type& field(const either_data<A, B>& data) { return data.ub; }
-    };
-
-    template <std::size_t I, typename A, typename B>
-    struct either_get: either_select<I, A, B> {
-        using typename either_select<I, A, B>::type;
-        using either_select<I, A, B>::field;
-
-        static typename type::reference unsafe_get(either_data<A, B>& data) {
-            return field(data).ref();
-        }
-
-        static typename type::const_reference unsafe_get(const either_data<A, B>& data) {
-            return field(data).cref();
-        }
-
-        static typename type::reference unsafe_get(char which, either_data<A, B>& data) {
-            if (I!=which) {
-                throw either_invalid_access();
-            }
-            return field(data).ref();
-        }
-
-        static typename type::const_reference unsafe_get(char which, const either_data<A, B>& data) {
-            if (I!=which) {
-                throw either_invalid_access();
-            }
-            return field(data).cref();
-        }
-
-        static typename type::pointer ptr(char which, either_data<A, B>& data) {
-            return I==which? field(data).ptr(): nullptr;
-        }
-
-        static typename type::const_pointer ptr(char which, const either_data<A, B>& data) {
-            return I==which? field(data).cptr(): nullptr;
-        }
-    };
-} // namespace detail
-
-constexpr std::size_t variant_npos = static_cast<std::size_t>(-1); // emulating C++17 variant type
-
-template <typename A, typename B>
-class either: public detail::either_data<A, B> {
-    using base = detail::either_data<A, B>;
-    using base::ua;
-    using base::ub;
-
-    template <std::size_t I>
-    using getter = detail::either_get<I, A, B>;
-
-    unsigned char which;
-
-public:
-    // default ctor if A is default-constructible or A is not and B is.
-    template <
-        typename A_ = A,
-        bool a_ = std::is_default_constructible<A_>::value,
-        bool b_ = std::is_default_constructible<B>::value,
-        typename = std::enable_if_t<a_ || (!a_ && b_)>,
-        std::size_t w_ = a_? 0: 1
-    >
-    either() noexcept(std::is_nothrow_default_constructible<typename getter<w_>::type>::value):
-        which(w_)
-    {
-        getter<w_>::field(*this).construct();
-    }
-
-    // implicit constructors from A and B values by copy or move
-    either(const A& a) noexcept(std::is_nothrow_copy_constructible<A>::value): which(0) {
-        getter<0>::field(*this).construct(a);
-    }
-
-    template <
-        typename B_ = B,
-        typename = std::enable_if_t<!std::is_same<A, B_>::value>
-    >
-    either(const B& b) noexcept(std::is_nothrow_copy_constructible<B>::value): which(1) {
-        getter<1>::field(*this).construct(b);
-    }
-
-    either(A&& a) noexcept(std::is_nothrow_move_constructible<A>::value): which(0) {
-        getter<0>::field(*this).construct(std::move(a));
-    }
-
-    template <
-        typename B_ = B,
-        typename = std::enable_if_t<!std::is_same<A, B_>::value>
-    >
-    either(B&& b) noexcept(std::is_nothrow_move_constructible<B>::value): which(1) {
-        getter<1>::field(*this).construct(std::move(b));
-    }
-
-    // copy constructor
-    either(const either& x)
-        noexcept(std::is_nothrow_copy_constructible<A>::value &&
-            std::is_nothrow_copy_constructible<B>::value):
-        which(x.which)
-    {
-        if (which==0) {
-            getter<0>::field(*this).construct(x.unsafe_get<0>());
-        }
-        else if (which==1) {
-            getter<1>::field(*this).construct(x.unsafe_get<1>());
-        }
-    }
-
-    // move constructor
-    either(either&& x)
-        noexcept(std::is_nothrow_move_constructible<A>::value &&
-            std::is_nothrow_move_constructible<B>::value):
-        which(x.which)
-    {
-        if (which==0) {
-            getter<0>::field(*this).construct(std::move(x.unsafe_get<0>()));
-        }
-        else if (which==1) {
-            getter<1>::field(*this).construct(std::move(x.unsafe_get<1>()));
-        }
-    }
-
-    // copy assignment
-    either& operator=(const either& x) {
-        if (this==&x) {
-            return *this;
-        }
-
-        switch (which) {
-        case 0:
-            if (x.which==0) {
-                unsafe_get<0>() = x.unsafe_get<0>();
-            }
-            else {
-                if (x.which==1) {
-                    B b_tmp(x.unsafe_get<1>());
-                    getter<0>::field(*this).destruct();
-                    which = (unsigned char)variant_npos;
-                    getter<1>::field(*this).construct(std::move(b_tmp));
-                    which = 1;
-                }
-                else {
-                    getter<0>::field(*this).destruct();
-                    which = (unsigned char)variant_npos;
-                }
-            }
-            break;
-        case 1:
-            if (x.which==1) {
-                unsafe_get<1>() = x.unsafe_get<1>();
-            }
-            else {
-                if (x.which==0) {
-                    A a_tmp(x.unsafe_get<0>());
-                    getter<1>::field(*this).destruct();
-                    which = (unsigned char)variant_npos;
-                    getter<0>::field(*this).construct(std::move(a_tmp));
-                    which = 0;
-                }
-                else {
-                    getter<1>::field(*this).destruct();
-                    which = (unsigned char)variant_npos;
-                }
-            }
-            break;
-        default: // variant_npos
-            if (x.which==0) {
-                getter<0>::field(*this).construct(x.unsafe_get<0>());
-            }
-            else if (x.which==1) {
-                getter<1>::field(*this).construct(x.unsafe_get<1>());
-            }
-            break;
-        }
-        return *this;
-    }
-
-    // move assignment
-    either& operator=(either&& x) {
-        if (this==&x) {
-            return *this;
-        }
-
-        switch (which) {
-        case 0:
-            if (x.which==0) {
-                unsafe_get<0>() = std::move(x.unsafe_get<0>());
-            }
-            else {
-                which = (unsigned char)variant_npos;
-                getter<0>::field(*this).destruct();
-                if (x.which==1) {
-                    getter<1>::field(*this).construct(std::move(x.unsafe_get<1>()));
-                    which = 1;
-                }
-            }
-            break;
-        case 1:
-            if (x.which==1) {
-                unsafe_get<1>() = std::move(x.unsafe_get<1>());
-            }
-            else {
-                which = (unsigned char)variant_npos;
-                getter<1>::field(*this).destruct();
-                if (x.which==0) {
-                    getter<0>::field(*this).construct(std::move(x.unsafe_get<0>()));
-                    which = 0;
-                }
-            }
-            break;
-        default: // variant_npos
-            if (x.which==0) {
-                getter<0>::field(*this).construct(std::move(x.unsafe_get<0>()));
-            }
-            else if (x.which==1) {
-                getter<1>::field(*this).construct(std::move(x.unsafe_get<1>()));
-            }
-            break;
-        }
-        return *this;
-    }
-
-    // unchecked element access
-    template <std::size_t I>
-    typename getter<I>::type::reference unsafe_get() {
-        return getter<I>::unsafe_get(*this);
-    }
-
-    template <std::size_t I>
-    typename getter<I>::type::const_reference unsafe_get() const {
-        return getter<I>::unsafe_get(*this);
-    }
-
-    // checked element access
-    template <std::size_t I>
-    typename getter<I>::type::reference get() {
-        return getter<I>::unsafe_get(which, *this);
-    }
-
-    template <std::size_t I>
-    typename getter<I>::type::const_reference get() const {
-        return getter<I>::unsafe_get(which, *this);
-    }
-
-    // convenience getter aliases
-    typename getter<0>::type::reference first() { return get<0>(); }
-    typename getter<0>::type::const_reference first() const { return get<0>(); }
-
-    typename getter<1>::type::reference second() { return get<1>(); }
-    typename getter<1>::type::const_reference second() const  { return get<1>(); }
-
-    // pointer to element access: return nullptr if it does not hold this item
-    template <std::size_t I>
-    auto ptr() {
-        return getter<I>::ptr(which, *this);
-    }
-
-    template <std::size_t I>
-    auto ptr() const {
-        return getter<I>::ptr(which, *this);
-    }
-
-    // true in bool context if holds first alternative
-    constexpr operator bool() const { return which==0; }
-
-    constexpr bool valueless_by_exception() const noexcept {
-        return which==(unsigned char)variant_npos;
-    }
-
-    constexpr std::size_t index() const noexcept {
-        return which;
-    }
-
-    ~either() {
-        if (which==0) {
-            getter<0>::field(*this).destruct();
-        }
-        else if (which==1) {
-            getter<1>::field(*this).destruct();
-        }
-    }
-
-    // comparison operators follow C++17 variant semantics
-    bool operator==(const either& x) const {
-        return index()==x.index() &&
-           index()==0? unsafe_get<0>()==x.unsafe_get<0>():
-           index()==1? unsafe_get<1>()==x.unsafe_get<1>():
-           true;
-    }
-
-    bool operator!=(const either& x) const {
-        return index()!=x.index() ||
-           index()==0? unsafe_get<0>()!=x.unsafe_get<0>():
-           index()==1? unsafe_get<1>()!=x.unsafe_get<1>():
-           false;
-    }
-
-    bool operator<(const either& x) const {
-        return !x.valueless_by_exception() &&
-           index()==0? (x.index()==1 || unsafe_get<0>()<x.unsafe_get<0>()):
-           index()==1? (x.index()!=0 && unsafe_get<1>()<x.unsafe_get<1>()):
-           true;
-    }
-
-    bool operator>=(const either& x) const {
-        return x.valueless_by_exception() ||
-           index()==0? (x.index()!=1 && unsafe_get<0>()>=x.unsafe_get<0>()):
-           index()==1? (x.index()==0 || unsafe_get<1>()>=x.unsafe_get<1>()):
-           false;
-    }
-
-    bool operator<=(const either& x) const {
-        return valueless_by_exception() ||
-           x.index()==0? (index()!=1 && unsafe_get<0>()<=x.unsafe_get<0>()):
-           x.index()==1? (index()==0 || unsafe_get<1>()<=x.unsafe_get<1>()):
-           false;
-    }
-
-    bool operator>(const either& x) const {
-        return !valueless_by_exception() &&
-           x.index()==0? (index()==1 || unsafe_get<0>()>x.unsafe_get<0>()):
-           x.index()==1? (index()!=0 && unsafe_get<1>()>x.unsafe_get<1>()):
-           true;
-    }
-};
-
-} // namespace util
-} // namespace arb
diff --git a/arbor/include/arbor/util/expected.hpp b/arbor/include/arbor/util/expected.hpp
new file mode 100644
index 00000000..be9dba14
--- /dev/null
+++ b/arbor/include/arbor/util/expected.hpp
@@ -0,0 +1,524 @@
+#pragma once
+
+// C++14 version of the proposed std::expected class
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0323r9.html
+//
+// Difference from proposal:
+//
+// * Left out lots of constexpr.
+// * Lazy about explicitness of some conversions.
+
+#include <initializer_list>
+#include <optional>
+#include <type_traits>
+#include <utility>
+#include <variant>
+
+namespace arb {
+namespace util {
+
+namespace detail {
+// True if T can constructed from or converted from [const, ref] X.
+template <typename T, typename X>
+struct conversion_hazard: std::integral_constant<bool,
+    std::is_constructible_v<T, X> ||
+    std::is_constructible_v<T, const X> ||
+    std::is_constructible_v<T, X&> ||
+    std::is_constructible_v<T, const X&> ||
+    std::is_convertible_v<X, T> ||
+    std::is_convertible_v<const X, T> ||
+    std::is_convertible_v<X&, T> ||
+    std::is_convertible_v<const X&, T>> {};
+
+template <typename T, typename X>
+inline constexpr bool conversion_hazard_v = conversion_hazard<T, X>::value;
+
+// TODO: C++20 replace with std::remove_cvref_t
+template <typename X>
+using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<X>>;
+} // namespace detail
+
+struct unexpect_t {};
+inline constexpr unexpect_t unexpect{};
+
+template <typename E = void>
+struct bad_expected_access;
+
+template <>
+struct bad_expected_access<void>: std::exception {
+    bad_expected_access() {}
+    virtual const char* what() const noexcept override { return "bad expected access"; }
+};
+
+template <typename E>
+struct bad_expected_access: public bad_expected_access<void> {
+    explicit bad_expected_access(E error): error_(error) {}
+
+    E& error() & { return error_; }
+    const E& error() const& { return error_; }
+    E&& error() && { return std::move(error_); }
+    const E&& error() const && { return std::move(error_); }
+
+private:
+    E error_;
+};
+
+// The unexpected<E> wrapper is mainly boiler-plate for a box that wraps
+// a value of type E, with corresponding conversion and assignment semantics.
+
+template <typename E>
+struct unexpected {
+    template <typename F>
+    friend class unexpected;
+
+    unexpected() = default;
+    unexpected(const unexpected&) = default;
+    unexpected(unexpected&&) = default;
+
+    // Emplace-style ctors.
+
+    template <typename... Args>
+    explicit unexpected(std::in_place_t, Args&&... args):
+        value_(std::forward<Args>(args)...) {}
+
+    template <typename X, typename... Args>
+    explicit unexpected(std::in_place_t, std::initializer_list<X> il, Args&&... args):
+        value_(il, std::forward<Args>(args)...) {}
+
+    // Converting ctors.
+
+    template <typename F,
+        typename = std::enable_if_t<std::is_constructible_v<E, F&&>>,
+        typename = std::enable_if_t<!std::is_same_v<std::in_place_t, detail::remove_cvref_t<F>>>,
+        typename = std::enable_if_t<!std::is_same_v<unexpected, detail::remove_cvref_t<F>>>
+    >
+    explicit unexpected(F&& f): value_(std::forward<F>(f)) {}
+
+    template <
+        typename F,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<E, unexpected<F>>>
+    >
+    unexpected(unexpected<F>&& u): value_(std::move(u.value_)) {}
+
+    template <
+        typename F,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<E, unexpected<F>>>
+    >
+    unexpected(const unexpected<F>& u): value_(u.value_) {}
+
+    // Assignment operators.
+
+    unexpected& operator=(const unexpected& u) { return value_ = u.value_, *this; }
+
+    unexpected& operator=(unexpected&& u) { return value_ = std::move(u.value_), *this; }
+
+    template <typename F>
+    unexpected& operator=(const unexpected<F>& u) { return value_ = u.value_, *this; }
+
+    template <typename F>
+    unexpected& operator=(unexpected<F>&& u) { return value_ = std::move(u.value_), *this; }
+
+    // Access.
+
+    E& value() & { return value_; }
+    const E& value() const & { return value_; }
+    E&& value() && { return std::move(value_); }
+    const E&& value() const && { return std::move(value_); }
+
+    // Equality.
+
+    template <typename F>
+    bool operator==(const unexpected<F>& other) const { return value()==other.value(); }
+
+    template <typename F>
+    bool operator!=(const unexpected<F>& other) const { return value()!=other.value(); }
+
+    // Swap.
+
+    void swap(unexpected& other) noexcept(std::is_nothrow_swappable_v<E>) {
+        using std::swap;
+        swap(value_, other.value_);
+    }
+
+    friend void swap(unexpected& a, unexpected& b) noexcept(noexcept(a.swap(b))) { a.swap(b); }
+
+private:
+    E value_;
+};
+
+template <typename E>
+unexpected(E) -> unexpected<E>;
+
+template <typename T, typename E>
+struct expected {
+    using value_type = T;
+    using error_type = E;
+    using unexpected_type = unexpected<E>;
+    using data_type = std::variant<T, unexpected_type>;
+
+    expected() = default;
+    expected(const expected&) = default;
+    expected(expected&&) = default;
+
+    // Emplace-style ctors.
+
+    template <typename... Args>
+    explicit expected(std::in_place_t, Args&&... args):
+        data_(std::in_place_index<0>, std::forward<Args>(args)...) {}
+
+    template <typename X, typename... Args>
+    explicit expected(std::in_place_t, std::initializer_list<X> il, Args&&... args):
+        data_(std::in_place_index<0>, il, std::forward<Args>(args)...) {}
+
+    // (Proposal says to forward args to unexpected<E>, but this is not compatible
+    // with the requirement that E is constructible from args; so here we're forwarding
+    // to unexpected<E> with an additional 'in_place' argument.)
+    template <typename... Args>
+    explicit expected(unexpect_t, Args&&... args):
+        data_(std::in_place_index<1>, std::in_place_t{}, std::forward<Args>(args)...) {}
+
+    template <typename X, typename... Args>
+    explicit expected(unexpect_t, std::initializer_list<X> il, Args&&... args):
+        data_(std::in_place_index<1>, std::in_place_t{}, il, std::forward<Args>(args)...) {}
+
+    // Converting ctors.
+
+    template <
+        typename S,
+        typename F,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<T, expected<S, F>>>,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<unexpected<E>, expected<S, F>>>
+    >
+    expected(const expected<S, F>& other):
+        data_(other? data_type(std::in_place_index<0>, *other): data_type(std::in_place_index<1>, other.error()))
+    {}
+
+    template <
+        typename S,
+        typename F,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<T, expected<S, F>>>,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<unexpected<E>, expected<S, F>>>
+    >
+    expected(expected<S, F>&& other):
+        data_(other? data_type(std::in_place_index<0>, *std::move(other)): data_type(std::in_place_index<1>, std::move(other).error()))
+    {}
+
+    template <
+        typename S,
+        typename = std::enable_if_t<std::is_constructible_v<T, S&&>>,
+        typename = std::enable_if_t<!std::is_same_v<std::in_place_t, detail::remove_cvref_t<S>>>,
+        typename = std::enable_if_t<!std::is_same_v<expected, detail::remove_cvref_t<S>>>,
+        typename = std::enable_if_t<!std::is_same_v<unexpected<E>, detail::remove_cvref_t<S>>>
+    >
+    expected(S&& x): data_(std::in_place_index<0>, std::forward<S>(x)) {}
+
+    template <typename F>
+    expected(const unexpected<F>& u): data_(std::in_place_index<1>, u) {}
+
+    template <typename F>
+    expected(unexpected<F>&& u): data_(std::in_place_index<1>, std::move(u)) {}
+
+    // Assignment ops.
+
+    expected& operator=(const expected& other) noexcept(std::is_nothrow_copy_assignable_v<data_type>) {
+        data_ = other.data_;
+        return *this;
+    }
+
+    expected& operator=(expected&& other) noexcept(std::is_nothrow_move_assignable_v<data_type>) {
+        data_ = std::move(other.data_);
+        return *this;
+    }
+
+    template <
+        typename S,
+        typename = std::enable_if_t<!std::is_same_v<expected, detail::remove_cvref_t<S>>>,
+        typename = std::enable_if_t<std::is_constructible_v<T, S>>,
+        typename = std::enable_if_t<std::is_assignable_v<T&, S>>
+    >
+    expected& operator=(S&& v) {
+        data_ = data_type(std::in_place_index<0>, std::forward<S>(v));
+        return *this;
+    }
+
+    template <typename F>
+    expected& operator=(const unexpected<F>& u) {
+        data_ = data_type(std::in_place_index<1>, u);
+        return *this;
+    }
+
+    template <typename F>
+    expected& operator=(unexpected<F>&& u) {
+        data_ = data_type(std::in_place_index<1>, std::move(u));
+        return *this;
+    }
+
+    // Emplace ops.
+
+    template <typename... Args>
+    T& emplace(Args&&... args) {
+        data_ = data_type(std::in_place_index<0>, std::forward<Args>(args)...);
+        return value();
+    }
+
+    template <typename U, typename... Args>
+    T& emplace(std::initializer_list<U> il, Args&&... args) {
+        data_ = data_type(std::in_place_index<0>, il, std::forward<Args>(args)...);
+        return value();
+    }
+
+    // Swap ops.
+
+    void swap(expected& other) noexcept(std::is_nothrow_swappable_v<data_type>) {
+        data_.swap(other.data_);
+    }
+
+    friend void swap(expected& a, expected& b) { a.swap(b); }
+
+    // Accessors.
+
+    bool has_value() const noexcept { return data_.index()==0; }
+    explicit operator bool() const noexcept { return has_value(); }
+
+    T& value() & {
+        if (*this) return std::get<0>(data_);
+        throw bad_expected_access(error());
+    }
+    const T& value() const& {
+        if (*this) return std::get<0>(data_);
+        throw bad_expected_access(error());
+    }
+
+    T&& value() && {
+        if (*this) return std::get<0>(std::move(data_));
+        throw bad_expected_access(error());
+    }
+    const T&& value() const&& {
+        if (*this) return std::get<0>(std::move(data_));
+        throw bad_expected_access(error());
+    }
+
+    const E& error() const& { return std::get<1>(data_).value(); }
+    E& error() & { return std::get<1>(data_).value(); }
+
+    const E&& error() const&& { return std::get<1>(std::move(data_)).value(); }
+    E&& error() && { return std::get<1>(std::move(data_)).value(); }
+
+    const T& operator*() const& { return std::get<0>(data_); }
+    T& operator*() & { return std::get<0>(data_); }
+
+    const T&& operator*() const&& { return std::get<0>(std::move(data_)); }
+    T&& operator*() && { return std::get<0>(std::move(data_)); }
+
+    const T* operator->() const { return std::get_if<0>(&data_); }
+    T* operator->() { return std::get_if<0>(&data_); }
+
+    template <typename S>
+    T value_or(S&& s) const& { return has_value()? value(): std::forward<S>(s); }
+
+    template <typename S>
+    T value_or(S&& s) && { return has_value()? value(): std::forward<S>(s); }
+
+private:
+    data_type data_;
+};
+
+// Equality comparisons for non-void expected.
+
+template <typename T1, typename E1, typename T2, typename E2,
+          typename = std::enable_if_t<!std::is_void_v<T1>>,
+          typename = std::enable_if_t<!std::is_void_v<T2>>>
+inline bool operator==(const expected<T1, E1>& a, const expected<T2, E2>& b) {
+    return a? b && a.value()==b.value(): !b && a.error()==b.error();
+}
+
+template <typename T1, typename E1, typename T2, typename E2,
+          typename = std::enable_if_t<!std::is_void_v<T1>>,
+          typename = std::enable_if_t<!std::is_void_v<T2>>>
+inline bool operator!=(const expected<T1, E1>& a, const expected<T2, E2>& b) {
+    return a? !b || a.value()!=b.value(): b || a.error()!=b.error();
+}
+
+template <typename T1, typename E1, typename T2,
+          typename = std::enable_if_t<!std::is_void_v<T1>>,
+          typename = decltype(static_cast<bool>(std::declval<const expected<T1, E1>>().value()==std::declval<T2>()))>
+inline bool operator==(const expected<T1, E1>& a, const T2& v) {
+    return a && a.value()==v;
+}
+
+template <typename T1, typename E1, typename T2,
+          typename = std::enable_if_t<!std::is_void_v<T1>>,
+          typename = decltype(static_cast<bool>(std::declval<const expected<T1, E1>>().value()==std::declval<T2>()))>
+inline bool operator==(const T2& v, const expected<T1, E1>& a) {
+    return a==v;
+}
+
+template <typename T1, typename E1, typename T2,
+          typename = std::enable_if_t<!std::is_void_v<T1>>,
+          typename = decltype(static_cast<bool>(std::declval<const expected<T1, E1>>().value()!=std::declval<T2>()))>
+inline bool operator!=(const expected<T1, E1>& a, const T2& v) {
+    return !a || a.value()!=v;
+}
+
+template <typename T1, typename E1, typename T2,
+          typename = std::enable_if_t<!std::is_void_v<T1>>,
+          typename = decltype(static_cast<bool>(std::declval<const expected<T1, E1>>().value()!=std::declval<T2>()))>
+inline bool operator!=(const T2& v, const expected<T1, E1>& a) {
+    return a!=v;
+}
+
+// Equality comparisons against unexpected.
+
+template <typename T1, typename E1, typename E2,
+          typename = decltype(static_cast<bool>(unexpected(std::declval<const expected<T1, E1>>().error())
+                                                ==std::declval<const unexpected<E2>>()))>
+inline bool operator==(const expected<T1, E1>& a, const unexpected<E2>& u) {
+    return !a && unexpected(a.error())==u;
+}
+
+template <typename T1, typename E1, typename E2,
+          typename = decltype(static_cast<bool>(unexpected(std::declval<const expected<T1, E1>>().error())
+                                                ==std::declval<const unexpected<E2>>()))>
+inline bool operator==(const unexpected<E2>& u, const expected<T1, E1>& a) {
+    return a==u;
+}
+
+template <typename T1, typename E1, typename E2,
+          typename = decltype(static_cast<bool>(unexpected(std::declval<const expected<T1, E1>>().error())
+                                                !=std::declval<const unexpected<E2>>()))>
+inline bool operator!=(const expected<T1, E1>& a, const unexpected<E2>& u) {
+    return a || unexpected(a.error())!=u;
+}
+
+template <typename T1, typename E1, typename E2,
+          typename = decltype(static_cast<bool>(unexpected(std::declval<const expected<T1, E1>>().error())
+                                                !=std::declval<const unexpected<E2>>()))>
+inline bool operator!=(const unexpected<E2>& u, const expected<T1, E1>& a) {
+    return a!=u;
+}
+
+
+template <typename E>
+struct expected<void, E> {
+    using value_type = void;
+    using error_type = E;
+    using unexpected_type = unexpected<E>;
+    using data_type = std::optional<unexpected_type>;
+
+    expected() = default;
+    expected(const expected&) = default;
+    expected(expected&&) = default;
+
+    // Emplace-style ctors.
+
+    explicit expected(std::in_place_t) {}
+
+    template <typename... Args>
+    explicit expected(unexpect_t, Args&&... args):
+        data_(std::in_place_t{}, std::in_place_t{}, std::forward<Args>(args)...) {}
+
+    template <typename X, typename... Args>
+    explicit expected(unexpect_t, std::initializer_list<X> il, Args&&... args):
+        data_(std::in_place_t{}, std::in_place_t{}, il, std::forward<Args>(args)...) {}
+
+    // Converting ctors.
+
+    template <
+        typename F,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<unexpected<E>, expected<void, F>>>
+    >
+    expected(const expected<void, F>& other): data_(other.data_) {}
+
+    template <
+        typename F,
+        typename = std::enable_if_t<!detail::conversion_hazard_v<unexpected<E>, expected<void, F>>>
+    >
+    expected(expected<void, F>&& other): data_(std::move(other.data_)) {}
+
+    template <typename F>
+    expected(const unexpected<F>& u): data_(u) {}
+
+    template <typename F>
+    expected(unexpected<F>&& u): data_(std::move(u)) {}
+
+    // Assignment ops.
+
+    expected& operator=(const expected& other) noexcept(std::is_nothrow_copy_assignable_v<data_type>) {
+        data_ = other.data_;
+        return *this;
+    }
+
+    expected& operator=(expected&& other) noexcept(std::is_nothrow_move_assignable_v<data_type>) {
+        data_ = std::move(other.data_);
+        return *this;
+    }
+
+    template <typename F>
+    expected& operator=(const unexpected<F>& u) {
+        data_ = u;
+        return *this;
+    }
+
+    template <typename F>
+    expected& operator=(unexpected<F>&& u) {
+        data_ = std::move(u);
+        return *this;
+    }
+
+    // No emplace ops.
+
+    // Swap ops.
+
+    void swap(expected& other) {
+        // TODO: C++17 just use std::optional::swap; haven't implemented util::optional::swap.
+        if (data_) {
+            if (other.data_) {
+                std::swap(*data_, *other.data_);
+            }
+            else {
+                other.data_ = std::move(data_);
+                data_.reset();
+            }
+        }
+        else if (other.data_) {
+            data_ = std::move(other.data_);
+            other.data_.reset();
+        }
+    }
+
+    // Accessors.
+
+    bool has_value() const noexcept { return !data_; }
+    explicit operator bool() const noexcept { return has_value(); }
+
+    void value() const {
+        if (!has_value()) throw bad_expected_access<E>(error());
+    }
+
+    const E& error() const& { return data_->value(); }
+    E& error() & { return data_->value(); }
+
+    const E&& error() const&& { return std::move(data_->value()); }
+    E&& error() && { return std::move(data_->value()); }
+
+private:
+    data_type data_;
+};
+
+// Equality comparisons for void expected.
+
+template <typename T1, typename E1, typename T2, typename E2,
+          typename = std::enable_if_t<std::is_void_v<T1> || std::is_void_v<T2>>>
+inline bool operator==(const expected<T1, E1>& a, const expected<T2, E2>& b) {
+    return a? b && std::is_void_v<T1> && std::is_void_v<T2>: !b && a.error()==b.error();
+}
+
+template <typename T1, typename E1, typename T2, typename E2,
+          typename = std::enable_if_t<std::is_void_v<T1> || std::is_void_v<T2>>>
+inline bool operator!=(const expected<T1, E1>& a, const expected<T2, E2>& b) {
+    return a? !b || !std::is_void_v<T2> || !std::is_void_v<T1>: b || a.error()!=b.error();
+}
+
+
+} // namespace util
+} // namespace arb
diff --git a/arbor/include/arbor/util/optional.hpp b/arbor/include/arbor/util/optional.hpp
index c314f3a8..96921676 100644
--- a/arbor/include/arbor/util/optional.hpp
+++ b/arbor/include/arbor/util/optional.hpp
@@ -14,8 +14,6 @@
  *
  *   4. `swap()` method and ADL-available `swap()` function.
  *
- *   5. In-place construction with `std::in_place_t` tags or equivalent.
- *
  *   5. No `make_optional` function (but see `just` below).
  *
  * Additional/differing functionality:
@@ -126,6 +124,13 @@ namespace detail {
             }
         }
 
+        template <typename... Args>
+        optional_base(bool set_, std::in_place_t, Args&&... args) : set(set_) {
+            if (set) {
+                data.construct(std::forward<Args>(args)...);
+            }
+        }
+
         reference       ref()       { return data.ref(); }
         const_reference ref() const { return data.cref(); }
 
@@ -208,6 +213,12 @@ struct optional: detail::optional_base<X> {
     optional(X&& x)
         noexcept(std::is_nothrow_move_constructible<X>::value): base(true, std::move(x)) {}
 
+    template <typename... Args>
+    optional(std::in_place_t, Args&&... args): base(true, std::in_place_t{}, std::forward<Args>(args)...) {}
+
+    template <typename U, typename... Args>
+    optional(std::in_place_t, std::initializer_list<U> il, Args&&... args): base(true, std::in_place_t{}, il, std::forward<Args>(args)...) {}
+
     optional(const optional& ot): base(ot.set, ot.ref()) {}
 
     template <typename T>
diff --git a/arbor/include/arbor/util/variant.hpp b/arbor/include/arbor/util/variant.hpp
index 1f3ca0ca..ecb724a1 100644
--- a/arbor/include/arbor/util/variant.hpp
+++ b/arbor/include/arbor/util/variant.hpp
@@ -172,7 +172,7 @@ struct variant_dynamic_impl<> {
         if (i!=std::size_t(-1)) throw bad_variant_access{};
     }
 
-    static void move_assign(std::size_t i, char* data, const char* from) {
+    static void move_assign(std::size_t i, char* data, char* from) {
         if (i!=std::size_t(-1)) throw bad_variant_access{};
     }
 
@@ -220,9 +220,9 @@ struct variant_dynamic_impl<H, T...> {
         }
     }
 
-    static void move_assign(std::size_t i, char* data, const char* from) {
+    static void move_assign(std::size_t i, char* data, char* from) {
         if (i==0) {
-            *reinterpret_cast<H*>(data) = std::move(*reinterpret_cast<const H*>(from));
+            *reinterpret_cast<H*>(data) = std::move(*reinterpret_cast<H*>(from));
         }
         else {
             variant_dynamic_impl<T...>::move_assign(i-1, data, from);
diff --git a/arbor/mechcat.cpp b/arbor/mechcat.cpp
index 4be6af12..ab30845c 100644
--- a/arbor/mechcat.cpp
+++ b/arbor/mechcat.cpp
@@ -6,7 +6,7 @@
 
 #include <arbor/arbexcept.hpp>
 #include <arbor/mechcat.hpp>
-#include <arbor/util/either.hpp>
+#include <arbor/util/expected.hpp>
 
 #include "util/maputil.hpp"
 
@@ -53,15 +53,14 @@
  * mechanism.
  *
  * The private implementation class catalogue_state does not throw any (catalogue
- * related) exceptions, but instead propagates errors via util::either to the
+ * related) exceptions, but instead propagates errors via util::expected to the
  * mechanism_catalogue methods for handling.
  */
 
 namespace arb {
 
 using util::value_by_key;
-using util::optional;
-using util::nullopt;
+using util::unexpected;
 
 using std::make_unique;
 using std::make_exception_ptr;
@@ -72,42 +71,26 @@ template <typename V>
 using string_map = std::unordered_map<std::string, V>;
 
 template <typename T>
-struct hopefully_typemap {
-    using type = util::either<T, std::exception_ptr>;
-};
-
-template <>
-struct hopefully_typemap<void> {
-    struct placeholder_type {};
-    using type = util::either<placeholder_type, std::exception_ptr>;
-};
-
-template <typename T>
-using hopefully = typename hopefully_typemap<T>::type;
+using hopefully = util::expected<T, std::exception_ptr>;
 
+namespace {
 // Convert hopefully<T> to T or throw.
-
 template <typename T>
-const T& value(const util::either<T, std::exception_ptr>& x) {
-    if (!x) {
-        std::rethrow_exception(x.second());
-    }
-    return x.first();
+const T& value(const hopefully<T>& x) {
+    if (!x) std::rethrow_exception(x.error());
+    return x.value();
 }
 
 template <typename T>
-T value(util::either<T, std::exception_ptr>&& x) {
-    if (!x) {
-        std::rethrow_exception(x.second());
-    }
-    return std::move(x.first());
+T value(hopefully<T>&& x) {
+    if (!x) std::rethrow_exception(std::move(x).error());
+    return std::move(x).value();
 }
 
-void value(const hopefully<void>& x) {
-    if (!x) {
-        std::rethrow_exception(x.second());
-    }
-}
+// Conveniently make an unexpected exception_ptr:
+template <typename X>
+auto unexpected_exception_ptr(X x) { return unexpected(make_exception_ptr(std::move(x))); }
+} // anonymous namespace
 
 struct derivation {
     std::string parent;
@@ -187,17 +170,16 @@ struct catalogue_state {
     // Register concrete mechanism for a back-end type.
     hopefully<void> register_impl(std::type_index tidx, const std::string& name, std::unique_ptr<mechanism> mech) {
         if (auto fptr = fingerprint_ptr(name)) {
-            if (mech->fingerprint()!=*fptr.first()) {
-                return make_exception_ptr(fingerprint_mismatch(name));
+            if (mech->fingerprint()!=*fptr.value()) {
+                return unexpected_exception_ptr(fingerprint_mismatch(name));
             }
 
             impl_map_[name][tidx] = std::move(mech);
+            return {};
         }
         else {
-            return fptr.second();
+            return unexpected(fptr.error());
         }
-
-        return {};
     }
 
     // Remove mechanism and its derivations and implementations.
@@ -234,10 +216,10 @@ struct catalogue_state {
             return *(p->get());
         }
         else if (auto deriv = derive(name)) {
-            return *(deriv.first().derived_info.get());
+            return *(deriv->derived_info.get());
         }
         else {
-            return deriv.second();
+            return unexpected(deriv.error());
         }
     }
 
@@ -249,10 +231,10 @@ struct catalogue_state {
 
         if (!defined(name)) {
             if ((implicit_deriv = derive(name))) {
-                base = &implicit_deriv.first().parent;
+                base = &implicit_deriv->parent;
             }
             else {
-                return implicit_deriv.second();
+                return unexpected(implicit_deriv.error());
             }
         }
 
@@ -274,10 +256,10 @@ struct catalogue_state {
         const std::vector<std::pair<std::string, std::string>>& ion_remap_vec) const
     {
         if (defined(name)) {
-            return make_exception_ptr(duplicate_mechanism(name));
+            return unexpected_exception_ptr(duplicate_mechanism(name));
         }
         else if (!defined(parent)) {
-            return make_exception_ptr(no_such_mechanism(parent));
+            return unexpected_exception_ptr(no_such_mechanism(parent));
         }
 
         string_map<std::string> ion_remap_map(ion_remap_vec.begin(), ion_remap_vec.end());
@@ -285,10 +267,10 @@ struct catalogue_state {
 
         mechanism_info_ptr new_info;
         if (auto parent_info = info(parent)) {
-            new_info.reset(new mechanism_info(parent_info.first()));
+            new_info.reset(new mechanism_info(parent_info.value()));
         }
         else {
-            return parent_info.second();
+            return unexpected(parent_info.error());
         }
 
         // Update global parameter values in info for derived mechanism.
@@ -299,11 +281,11 @@ struct catalogue_state {
 
             if (auto p = value_by_key(new_info->globals, param)) {
                 if (!p->valid(value)) {
-                    return make_exception_ptr(invalid_parameter_value(name, param, value));
+                    return unexpected_exception_ptr(invalid_parameter_value(name, param, value));
                 }
             }
             else {
-                return make_exception_ptr(no_such_parameter(name, param));
+                return unexpected_exception_ptr(no_such_parameter(name, param));
             }
 
             deriv.globals[param] = value;
@@ -312,7 +294,7 @@ struct catalogue_state {
 
         for (const auto& kv: ion_remap_vec) {
             if (!new_info->ions.count(kv.first)) {
-                return make_exception_ptr(invalid_ion_remap(name, kv.first, kv.second));
+                return unexpected_exception_ptr(invalid_ion_remap(name, kv.first, kv.second));
             }
         }
 
@@ -322,7 +304,7 @@ struct catalogue_state {
         for (const auto& kv: new_info->ions) {
             if (auto new_ion = value_by_key(ion_remap_map, kv.first)) {
                 if (!new_ions.insert({*new_ion, kv.second}).second) {
-                    return make_exception_ptr(invalid_ion_remap(name, kv.first, *new_ion));
+                    return unexpected_exception_ptr(invalid_ion_remap(name, kv.first, *new_ion));
                 }
             }
             else {
@@ -330,7 +312,7 @@ struct catalogue_state {
                     // (find offending remap to report in exception)
                     for (const auto& entry: ion_remap_map) {
                         if (entry.second==kv.first) {
-                            return make_exception_ptr(invalid_ion_remap(name, kv.first, entry.second));
+                            return unexpected_exception_ptr(invalid_ion_remap(name, kv.first, entry.second));
                         }
                     }
                     throw arbor_internal_error("inconsistent catalogue ion remap state");
@@ -340,23 +322,23 @@ struct catalogue_state {
         new_info->ions = std::move(new_ions);
 
         deriv.derived_info = std::move(new_info);
-        return deriv;
+        return std::move(deriv);
     }
 
     // Implicit derivation.
     hopefully<derivation> derive(const std::string& name) const {
         if (defined(name)) {
-            return make_exception_ptr(duplicate_mechanism(name));
+            return unexpected_exception_ptr(duplicate_mechanism(name));
         }
 
         auto i = name.find_last_of('/');
         if (i==std::string::npos) {
-            return make_exception_ptr(no_such_mechanism(name));
+            return unexpected_exception_ptr(no_such_mechanism(name));
         }
 
         std::string base = name.substr(0, i);
         if (!defined(base)) {
-            return make_exception_ptr(no_such_mechanism(base));
+            return unexpected_exception_ptr(no_such_mechanism(base));
         }
 
         std::string suffix = name.substr(i+1);
@@ -385,7 +367,7 @@ struct catalogue_state {
             auto eq = assign.find('=');
             if (eq==std::string::npos) {
                 if (!single_ion) {
-                    return make_exception_ptr(invalid_ion_remap(assign));
+                    return unexpected_exception_ptr(invalid_ion_remap(assign));
                 }
 
                 k = info->ions.begin()->first;
@@ -403,7 +385,7 @@ struct catalogue_state {
                 char* end = 0;
                 double v_value = std::strtod(v.c_str(), &end);
                 if (!end || *end) {
-                    return make_exception_ptr(invalid_parameter_value(name, k, v));
+                    return unexpected_exception_ptr(invalid_parameter_value(name, k, v));
                 }
                 global_params.push_back({k, v_value});
             }
@@ -420,9 +402,9 @@ struct catalogue_state {
         if (!defined(name)) {
             implicit_deriv = derive(name);
             if (!implicit_deriv) {
-                return implicit_deriv.second();
+                return unexpected(implicit_deriv.error());
             }
-            impl_name = &implicit_deriv.first().parent;
+            impl_name = &implicit_deriv->parent;
         }
 
         for (;;) {
@@ -437,7 +419,7 @@ struct catalogue_state {
                 impl_name = &p->parent;
             }
             else {
-                return make_exception_ptr(no_such_implementation(name));
+                return unexpected_exception_ptr(no_such_implementation(name));
             }
         }
     }
@@ -482,10 +464,10 @@ struct catalogue_state {
         util::optional<derivation> implicit_deriv;
         if (!defined(name)) {
             if (auto deriv = derive(name)) {
-                implicit_deriv = std::move(deriv.first());
+                implicit_deriv = std::move(deriv.value());
             }
             else {
-                return deriv.second();
+                return unexpected(deriv.error());
             }
         }
 
diff --git a/arbor/morph/mprovider.cpp b/arbor/morph/mprovider.cpp
index 38e0226c..330fbf09 100644
--- a/arbor/morph/mprovider.cpp
+++ b/arbor/morph/mprovider.cpp
@@ -7,6 +7,7 @@
 #include <arbor/morph/mprovider.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/region.hpp>
+#include <arbor/util/expected.hpp>
 
 namespace arb {
 
@@ -35,19 +36,19 @@ void mprovider::init() {
 // label_dict_ptr will be null, and concrete regions/locsets will only be retrieved
 // from the maps established during initialization.
 
-template <typename RegOrLocMap, typename LabelDictMap, typename Err>
-static const auto& try_lookup(const mprovider& provider, const std::string& name, RegOrLocMap& map, const LabelDictMap* dict_ptr, Err errval) {
+template <typename RegOrLocMap, typename LabelDictMap>
+static const auto& try_lookup(const mprovider& provider, const std::string& name, RegOrLocMap& map, const LabelDictMap* dict_ptr) {
     auto it = map.find(name);
     if (it==map.end()) {
         if (dict_ptr) {
-            map.emplace(name, errval);
+            map.emplace(name, util::unexpect);
 
             auto it = dict_ptr->find(name);
             if (it==dict_ptr->end()) {
                 throw unbound_name(name);
             }
 
-            return (map[name] = thingify(it->second, provider)).first();
+            return (map[name] = thingify(it->second, provider)).value();
         }
         else {
             throw unbound_name(name);
@@ -57,18 +58,18 @@ static const auto& try_lookup(const mprovider& provider, const std::string& name
         throw circular_definition(name);
     }
     else {
-        return it->second.first();
+        return it->second.value();
     }
 }
 
 const mextent& mprovider::region(const std::string& name) const {
     const auto* regions_ptr = label_dict_ptr? &(label_dict_ptr->regions()): nullptr;
-    return try_lookup(*this, name, regions_, regions_ptr, circular_def{});
+    return try_lookup(*this, name, regions_, regions_ptr);
 }
 
 const mlocation_list& mprovider::locset(const std::string& name) const {
     const auto* locsets_ptr = label_dict_ptr? &(label_dict_ptr->locsets()): nullptr;
-    return try_lookup(*this, name, locsets_, locsets_ptr, circular_def{});
+    return try_lookup(*this, name, locsets_, locsets_ptr);
 }
 
 
diff --git a/arbor/util/filter.hpp b/arbor/util/filter.hpp
index 43f6fd88..32ea5516 100644
--- a/arbor/util/filter.hpp
+++ b/arbor/util/filter.hpp
@@ -9,11 +9,11 @@
 #include <type_traits>
 
 #include <arbor/assert.hpp>
+#include <arbor/util/uninitialized.hpp>
 
-#include <util/iterutil.hpp>
-#include <util/meta.hpp>
-#include <util/range.hpp>
-
+#include "util/iterutil.hpp"
+#include "util/meta.hpp"
+#include "util/range.hpp"
 
 namespace arb {
 namespace util {
diff --git a/arbor/util/partition.hpp b/arbor/util/partition.hpp
index f6d4f67c..14a50d68 100644
--- a/arbor/util/partition.hpp
+++ b/arbor/util/partition.hpp
@@ -5,7 +5,7 @@
 #include <stdexcept>
 #include <type_traits>
 
-#include <arbor/util/either.hpp>
+#include <arbor/util/expected.hpp>
 
 #include "util/meta.hpp"
 #include "util/partition_iterator.hpp"
@@ -52,7 +52,7 @@ public:
     void validate() const {
         auto ok = is_valid();
         if (!ok) {
-            throw invalid_partition(ok.second());
+            throw invalid_partition(ok.error());
         }
     }
 
@@ -86,13 +86,11 @@ public:
     }
 
 private:
-    either<bool, std::string> is_valid() const {
+    expected<void, std::string> is_valid() const {
         if (!std::is_sorted(left.get(), right.get())) {
-            return std::string("offsets are not monotonically increasing");
-        }
-        else {
-            return true;
+            return unexpected(std::string("offsets are not monotonically increasing"));
         }
+        return {};
     }
 };
 
diff --git a/arbor/util/sentinel.hpp b/arbor/util/sentinel.hpp
index 9b1bf1a2..30d0696d 100644
--- a/arbor/util/sentinel.hpp
+++ b/arbor/util/sentinel.hpp
@@ -2,7 +2,7 @@
 
 #include <type_traits>
 
-#include <arbor/util/either.hpp>
+#include <arbor/util/variant.hpp>
 
 #include "util/meta.hpp"
 
@@ -28,26 +28,26 @@ struct iterator_category_select<I,S,true> {
 
 template <typename I, typename S>
 class sentinel_iterator {
-    arb::util::either<I, S> e_;
+    arb::util::variant<I, S> e_;
 
     I& iter() {
         arb_assert(!is_sentinel());
-        return e_.template unsafe_get<0>();
+        return get<0>(e_);
     }
 
     const I& iter() const {
         arb_assert(!is_sentinel());
-        return e_.template unsafe_get<0>();
+        return get<0>(e_);
     }
 
     S& sentinel() {
         arb_assert(is_sentinel());
-        return e_.template unsafe_get<1>();
+        return get<1>(e_);
     }
 
     const S& sentinel() const {
         arb_assert(is_sentinel());
-        return e_.template unsafe_get<1>();
+        return get<1>(e_);
     }
 
 public:
diff --git a/arbor/util/transform.hpp b/arbor/util/transform.hpp
index ed436973..747c5c95 100644
--- a/arbor/util/transform.hpp
+++ b/arbor/util/transform.hpp
@@ -9,9 +9,11 @@
 #include <memory>
 #include <type_traits>
 
-#include <util/iterutil.hpp>
-#include <util/meta.hpp>
-#include <util/range.hpp>
+#include <arbor/util/uninitialized.hpp>
+
+#include "util/iterutil.hpp"
+#include "util/meta.hpp"
+#include "util/range.hpp"
 
 namespace arb {
 namespace util {
diff --git a/python/error.hpp b/python/error.hpp
index c97dad43..8a650b8a 100644
--- a/python/error.hpp
+++ b/python/error.hpp
@@ -7,7 +7,7 @@
 #include <pybind11/pybind11.h>
 
 #include <arbor/arbexcept.hpp>
-#include <arbor/util/either.hpp>
+#include <arbor/util/expected.hpp>
 
 #include "strprintf.hpp"
 
@@ -57,12 +57,12 @@ template <typename T, typename E>
 struct hopefully {
     using value_type = T;
     using error_type = E;
-    arb::util::either<value_type, error_type> state;
+    arb::util::expected<value_type, error_type> state;
 
     hopefully(const hopefully&) = default;
 
     hopefully(value_type x): state(std::move(x)) {}
-    hopefully(error_type x): state(std::move(x)) {}
+    hopefully(error_type x): state(arb::util::unexpect, std::move(x)) {}
 
     const value_type& operator*() const {
         return try_get();
@@ -83,9 +83,9 @@ struct hopefully {
 
     const error_type& error() const {
         try {
-            return state.template get<1>();
+            return state.error();
         }
-        catch(arb::util::either_invalid_access& e) {
+        catch(arb::util::bad_expected_access<void>& e) {
             throw arb::arbor_internal_error(
                 "Attempt to get an error from a valid hopefully wrapper.");
         }
@@ -95,9 +95,9 @@ private:
 
     const value_type& try_get() const {
         try {
-            return state.template get<0>();
+            return state.value();
         }
-        catch(arb::util::either_invalid_access& e) {
+        catch(arb::util::bad_expected_access<void>& e) {
             throw arbor_internal_error(util::pprintf(
                 "Attempt to unwrap a hopefully with error state '{}'",
                 error().message));
@@ -105,9 +105,9 @@ private:
     }
     value_type& try_get() {
         try {
-            return state.template get<0>();
+            return state.value();
         }
-        catch(arb::util::either_invalid_access& e) {
+        catch(arb::util::bad_expected_access<void>& e) {
             throw arb::arbor_internal_error(util::pprintf(
                 "Attempt to unwrap a hopefully with error state '{}'",
                 error().message));
diff --git a/python/s_expr.hpp b/python/s_expr.hpp
index 8c4dd433..bf6998a1 100644
--- a/python/s_expr.hpp
+++ b/python/s_expr.hpp
@@ -4,7 +4,6 @@
 #include <memory>
 #include <vector>
 
-#include <arbor/util/either.hpp>
 #include <arbor/util/variant.hpp>
 
 namespace pyarb {
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index f988f32e..43d4eda8 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -95,11 +95,11 @@ set(unit_sources
     test_domain_decomposition.cpp
     test_dry_run_context.cpp
     test_double_buffer.cpp
-    test_either.cpp
     test_event_binner.cpp
     test_event_delivery.cpp
     test_event_generators.cpp
     test_event_queue.cpp
+    test_expected.cpp
     test_filter.cpp
     test_fvm_layout.cpp
     test_fvm_lowered.cpp
diff --git a/test/unit/test_either.cpp b/test/unit/test_either.cpp
deleted file mode 100644
index 4d18d47c..00000000
--- a/test/unit/test_either.cpp
+++ /dev/null
@@ -1,65 +0,0 @@
-#include <typeinfo>
-#include <array>
-#include <algorithm>
-
-#include <arbor/util/either.hpp>
-
-#include "../gtest.h"
-
-// TODO: coverage!
-
-using namespace arb::util;
-
-TEST(either, basic) {
-    either<int, std::string> e0(17);
-
-    EXPECT_TRUE(e0);
-    EXPECT_EQ(17, e0.get<0>());
-    EXPECT_EQ(e0.unsafe_get<0>(), e0.get<0>());
-    EXPECT_EQ(e0.unsafe_get<0>(), e0.first());
-    EXPECT_THROW(e0.get<1>(), either_invalid_access);
-    either<int, std::string> e1("seventeen");
-
-    EXPECT_FALSE(e1);
-    EXPECT_EQ("seventeen", e1.get<1>());
-    EXPECT_EQ(e1.unsafe_get<1>(), e1.get<1>());
-    EXPECT_EQ(e1.unsafe_get<1>(), e1.second());
-    EXPECT_THROW(e1.get<0>(), either_invalid_access);
-
-    e0 = e1;
-    EXPECT_EQ("seventeen", e0.get<1>());
-    EXPECT_THROW(e0.get<0>(), either_invalid_access);
-
-    e0 = 19;
-    EXPECT_EQ(19, e0.get<0>());
-}
-
-struct no_copy {
-    int value;
-
-    no_copy(): value(23) {}
-    explicit no_copy(int v): value(v) {}
-    no_copy(const no_copy&) = delete;
-    no_copy(no_copy&&) = default;
-
-    no_copy& operator=(const no_copy&) = delete;
-    no_copy& operator=(no_copy&&) = default;
-};
-
-TEST(either, no_copy) {
-    either<no_copy, std::string> e0(no_copy{17});
-
-    EXPECT_TRUE(e0);
-
-    either<no_copy, std::string> e1(std::move(e0));
-
-    EXPECT_TRUE(e1);
-
-    either<no_copy, std::string> e2;
-    EXPECT_TRUE(e2);
-    EXPECT_EQ(23, e2.get<0>().value);
-
-    e2 = std::move(e1);
-    EXPECT_TRUE(e2);
-    EXPECT_EQ(17, e2.get<0>().value);
-}
diff --git a/test/unit/test_expected.cpp b/test/unit/test_expected.cpp
new file mode 100644
index 00000000..1ca53f2b
--- /dev/null
+++ b/test/unit/test_expected.cpp
@@ -0,0 +1,348 @@
+#include "../gtest.h"
+
+#include <utility>
+#include <vector>
+#include <arbor/util/expected.hpp>
+
+#include "common.hpp"
+
+using namespace arb::util;
+using std::in_place;
+
+TEST(expected, ctors) {
+    // Test constructors, verify against bool conversion and
+    // access via value() and error().
+
+    struct int3 {
+        int v = 3;
+        int3() = default;
+        int3(int a, int b, int c): v(a+b+c) {}
+    };
+
+    {
+        // Default construction.
+
+        expected<int3, int3> x;
+        EXPECT_TRUE(x);
+        EXPECT_EQ(3, x.value().v);
+    }
+    {
+        // Default void construction.
+
+        expected<void, int3> x;
+        EXPECT_TRUE(x);
+    }
+    {
+        // In-place construction.
+
+        expected<int3, int3> x(in_place, 1, 2, 3);
+        EXPECT_TRUE(x);
+        EXPECT_EQ(6, x.value().v);
+    }
+    {
+        // From-value construction.
+
+        int3 v;
+        v.v = 19;
+        expected<int3, int3> x(v);
+        EXPECT_TRUE(x);
+        EXPECT_EQ(19, x.value().v);
+    }
+    {
+        // From-unexpected construction.
+
+        int3 v;
+        v.v = 19;
+        expected<int3, int3> x{unexpected(v)};
+        EXPECT_FALSE(x);
+        EXPECT_EQ(19, x.error().v);
+        EXPECT_THROW(x.value(), bad_expected_access<int3>);
+    }
+    {
+        // From-unexpected void construction.
+
+        int3 v;
+        v.v = 19;
+        expected<void, int3> x{unexpected(v)};
+        EXPECT_FALSE(x);
+        EXPECT_EQ(19, x.error().v);
+    }
+    {
+        // In-place unexpected construction.
+
+        expected<int3, int3> x(unexpect, 1, 2, 3);
+        EXPECT_FALSE(x);
+        EXPECT_EQ(6, x.error().v);
+        EXPECT_THROW(x.value(), bad_expected_access<int3>);
+    }
+    {
+        // In-place void unexpected construction.
+
+        expected<void, int3> x(unexpect, 1, 2, 3);
+        EXPECT_FALSE(x);
+        EXPECT_EQ(6, x.error().v);
+    }
+    {
+        // Conversion from other expect.
+
+        struct X {};
+        struct Y {};
+        struct Z {
+            int v = 0;
+            Z(const X&): v(1) {}
+            Z(const Y&): v(2) {}
+            Z(X&&): v(-1) {}
+            Z(Y&&): v(-2) {}
+        };
+
+        expected<X, Y> x;
+        expected<Z, Z> y(x);
+        EXPECT_TRUE(y);
+        EXPECT_EQ(1, y.value().v);
+
+        expected<Z, Z> my(std::move(x));
+        EXPECT_TRUE(my);
+        EXPECT_EQ(-1, my.value().v);
+
+        expected<X, Y> xu(unexpect);
+        expected<Z, Z> yu(xu);
+        EXPECT_FALSE(yu);
+        EXPECT_EQ(2, yu.error().v);
+
+        expected<Z, Z> myu(std::move(xu));
+        EXPECT_FALSE(myu);
+        EXPECT_EQ(-2, myu.error().v);
+    }
+}
+
+TEST(expected, assignment) {
+    {
+        expected<int, int> x(10), y(12), z(unexpect, 20);
+
+        EXPECT_EQ(12, (x=y).value());
+        EXPECT_EQ(20, (x=z).error());
+
+        expected<void, int> u, v, w(unexpect, 30);
+
+        EXPECT_TRUE((u=v).has_value());
+        EXPECT_EQ(30, (u=w).error());
+    }
+
+    {
+        struct X {
+            X(): v(0) {}
+            X(const int& a): v(10*a) {}
+            X(int&& a): v(20*a) {}
+            int v;
+        };
+
+        expected<X, int> y;
+        EXPECT_EQ(20, (y=1).value().v);
+        int a = 3;
+        EXPECT_EQ(30, (y=a).value().v);
+
+        expected<int, X> z;
+        EXPECT_EQ(20, (z=unexpected(1)).error().v);
+        unexpected<int> b(3);
+        EXPECT_EQ(30, (z=b).error().v);
+
+        expected<void, X> v;
+        EXPECT_EQ(20, (v=unexpected(1)).error().v);
+        EXPECT_EQ(30, (v=b).error().v);
+    }
+}
+
+TEST(expected, emplace) {
+    // Check we're forwarding properly...
+    struct X {
+        X(): v(0) {}
+        X(const int& a, int b): v(10*a + b) {}
+        X(int&& a, int b): v(20*a + b) {}
+        int v;
+    };
+
+    expected<X, bool> ex;
+    EXPECT_TRUE(ex);
+    EXPECT_EQ(0, ex.value().v);
+
+    int i = 3, j = 4;
+    ex.emplace(i, j);
+    EXPECT_TRUE(ex);
+    EXPECT_EQ(34, ex.value().v);
+    ex.emplace(3, j);
+    EXPECT_TRUE(ex);
+    EXPECT_EQ(64, ex.value().v);
+
+    // Should also work if ex was in error state.
+    expected<X, bool> ux(unexpect);
+    EXPECT_FALSE(ux);
+    ux.emplace(4, 1);
+    EXPECT_TRUE(ux);
+    EXPECT_EQ(81, ux.value().v);
+}
+
+TEST(expected, equality) {
+    {
+        // non-void value expected comparisons:
+
+        expected<int, int> ex1(1), ux1(unexpect, 1), ex2(2), ux2(unexpect, 2);
+        expected<int, int> x(ex1);
+
+        EXPECT_TRUE(x==ex1);
+        EXPECT_TRUE(ex1==x);
+        EXPECT_FALSE(x!=ex1);
+        EXPECT_FALSE(ex1!=x);
+
+        EXPECT_FALSE(x==ex2);
+        EXPECT_FALSE(ex2==x);
+        EXPECT_TRUE(x!=ex2);
+        EXPECT_TRUE(ex2!=x);
+
+        EXPECT_FALSE(x==ux1);
+        EXPECT_FALSE(ux1==x);
+        EXPECT_TRUE(x!=ux1);
+        EXPECT_TRUE(ux1!=x);
+
+        EXPECT_FALSE(ux1==ux2);
+        EXPECT_FALSE(ux2==ux1);
+        EXPECT_TRUE(ux1!=ux2);
+        EXPECT_TRUE(ux2!=ux1);
+    }
+    {
+        // non-void comparison against values and unexpected.
+
+        expected<int, int> x(10);
+
+        EXPECT_TRUE(x==10);
+        EXPECT_TRUE(10==x);
+        EXPECT_FALSE(x!=10);
+        EXPECT_FALSE(10!=x);
+
+        EXPECT_FALSE(x==unexpected(10));
+        EXPECT_FALSE(unexpected(10)==x);
+        EXPECT_TRUE(x!=unexpected(10));
+        EXPECT_TRUE(unexpected(10)!=x);
+
+        x = unexpected(10);
+
+        EXPECT_FALSE(x==10);
+        EXPECT_FALSE(10==x);
+        EXPECT_TRUE(x!=10);
+        EXPECT_TRUE(10!=x);
+
+        EXPECT_TRUE(x==unexpected(10));
+        EXPECT_TRUE(unexpected(10)==x);
+        EXPECT_FALSE(x!=unexpected(10));
+        EXPECT_FALSE(unexpected(10)!=x);
+    }
+    {
+        // void value expected comparisons:
+
+        expected<void, int> ev, uv1(unexpect, 1), uv2(unexpect, 2);
+        expected<void, int> x(ev);
+
+        EXPECT_TRUE(x==ev);
+        EXPECT_TRUE(ev==x);
+        EXPECT_FALSE(x!=ev);
+        EXPECT_FALSE(ev!=x);
+
+        EXPECT_FALSE(x==uv1);
+        EXPECT_FALSE(uv1==x);
+        EXPECT_TRUE(x!=uv1);
+        EXPECT_TRUE(uv1!=x);
+
+        EXPECT_FALSE(uv1==uv2);
+        EXPECT_FALSE(uv2==uv1);
+        EXPECT_TRUE(uv1!=uv2);
+        EXPECT_TRUE(uv2!=uv1);
+    }
+    {
+        // void value but difference unexpected types:
+
+        expected<void, int> uvi(unexpect);
+        expected<void, double> uvd(unexpect, 3.);
+
+        EXPECT_FALSE(uvi==uvd);
+        EXPECT_FALSE(uvd==uvi);
+
+        uvi = expected<void, int>();
+        ASSERT_TRUE(uvi);
+        EXPECT_FALSE(uvi==uvd);
+        EXPECT_FALSE(uvd==uvi);
+
+        uvd = expected<void, double>();
+        ASSERT_TRUE(uvd);
+        EXPECT_TRUE(uvi==uvd);
+        EXPECT_TRUE(uvd==uvi);
+    }
+    {
+        // void comparison against unexpected.
+
+        expected<void, int> x;
+
+        EXPECT_TRUE(x);
+        EXPECT_FALSE(x==unexpected(10));
+        EXPECT_FALSE(unexpected(10)==x);
+        EXPECT_TRUE(x!=unexpected(10));
+        EXPECT_TRUE(unexpected(10)!=x);
+
+        x = unexpected<int>(10);
+
+        EXPECT_FALSE(x);
+        EXPECT_TRUE(x==unexpected(10));
+        EXPECT_TRUE(unexpected(10)==x);
+        EXPECT_FALSE(x!=unexpected(10));
+        EXPECT_FALSE(unexpected(10)!=x);
+    }
+}
+
+TEST(expected, value_or) {
+    expected<double, char> a(2.0), b(unexpect, 'x');
+    EXPECT_EQ(2.0, a.value_or(1));
+    EXPECT_EQ(1.0, b.value_or(1));
+}
+
+namespace {
+struct Xswap {
+    explicit Xswap(int val, int& r): val(val), n_swap_ptr(&r) {}
+    int val;
+    int* n_swap_ptr;
+    void swap(Xswap& other) {
+        ++*n_swap_ptr;
+        std::swap(val, other.val);
+    }
+    friend void swap(Xswap& x1, Xswap& x2) noexcept { x1.swap(x2); }
+};
+
+struct swap_can_throw {
+    friend void swap(swap_can_throw& s1, swap_can_throw& s2) noexcept(false) {}
+};
+}
+
+TEST(expected, swap) {
+    int swaps = 0;
+    expected<Xswap, int> x1(in_place, -1, swaps), x2(in_place, -2, swaps);
+
+    using std::swap;
+    swap(x1, x2);
+    EXPECT_EQ(-2, x1->val);
+    EXPECT_EQ(-1, x2->val);
+    EXPECT_EQ(1, swaps);
+
+    swaps = 0;
+    expected<Xswap, int> x3(unexpect, 4);
+    swap(x1, x3);
+    EXPECT_EQ(4, x1.error());
+    EXPECT_EQ(-2, x3->val);
+    EXPECT_EQ(0, swaps); // Xswap is moved, not swapped.
+
+    swaps = 0;
+    unexpected<Xswap> u1(in_place, -1, swaps), u2(in_place, -2, swaps);
+    swap(u1, u2);
+    EXPECT_EQ(-2, u1.value().val);
+    EXPECT_EQ(-1, u2.value().val);
+    EXPECT_EQ(1, swaps);
+
+    EXPECT_TRUE(std::is_nothrow_swappable<unexpected<Xswap>>::value);
+    EXPECT_FALSE(std::is_nothrow_swappable<unexpected<swap_can_throw>>::value);
+}
-- 
GitLab