diff --git a/arbor/backends/gpu/mechanism.cpp b/arbor/backends/gpu/mechanism.cpp
index e6c0b46e71f29605ebd06ec0d97b858ba0d0e4b1..5aa114ecf81bc04f6f4f99661cee16dab774d0f4 100644
--- a/arbor/backends/gpu/mechanism.cpp
+++ b/arbor/backends/gpu/mechanism.cpp
@@ -1,6 +1,7 @@
 #include <algorithm>
 #include <cstddef>
 #include <cmath>
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
@@ -10,7 +11,6 @@
 #include <arbor/fvm_types.hpp>
 #include <arbor/math.hpp>
 #include <arbor/mechanism.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "memory/memory.hpp"
 #include "util/index_into.hpp"
@@ -25,8 +25,9 @@ namespace arb {
 namespace gpu {
 
 using memory::make_const_view;
-using util::value_by_key;
 using util::make_span;
+using util::ptr_by_key;
+using util::value_by_key;
 
 template <typename T>
 memory::device_view<T> device_view(T* ptr, std::size_t n) {
@@ -101,7 +102,7 @@ void mechanism::instantiate(unsigned id,
     for (auto i: ion_state_tbl) {
         auto ion_binding = value_by_key(overrides.ion_rebind, i.first).value_or(i.first);
 
-        util::optional<ion_state&> oion = value_by_key(shared.ion_data, ion_binding);
+        ion_state* oion = ptr_by_key(shared.ion_data, ion_binding);
         if (!oion) {
             throw arbor_internal_error("gpu/mechanism: mechanism holds ion with no corresponding shared state");
         }
@@ -156,7 +157,7 @@ void mechanism::instantiate(unsigned id,
     for (auto i: make_span(0, num_ions_)) {
         auto ion_binding = value_by_key(overrides.ion_rebind, ion_index_tbl[i].first).value_or(ion_index_tbl[i].first);
 
-        util::optional<ion_state&> oion = value_by_key(shared.ion_data, ion_binding);
+        ion_state* oion = ptr_by_key(shared.ion_data, ion_binding);
         if (!oion) {
             throw arbor_internal_error("gpu/mechanism: mechanism holds ion with no corresponding shared state");
         }
diff --git a/arbor/backends/multicore/mechanism.cpp b/arbor/backends/multicore/mechanism.cpp
index 63f50a362949aee2ed5f34433842ce0109cc2fbd..2aec088c825c1091fe1d918d498a79e0ba51d387 100644
--- a/arbor/backends/multicore/mechanism.cpp
+++ b/arbor/backends/multicore/mechanism.cpp
@@ -1,6 +1,7 @@
 #include <algorithm>
 #include <cstddef>
 #include <cmath>
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
@@ -9,7 +10,6 @@
 #include <arbor/common_types.hpp>
 #include <arbor/math.hpp>
 #include <arbor/mechanism.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "util/index_into.hpp"
 #include "util/maputil.hpp"
@@ -26,6 +26,7 @@ namespace arb {
 namespace multicore {
 
 using util::make_range;
+using util::ptr_by_key;
 using util::value_by_key;
 
 // Copy elements from source sequence into destination sequence,
@@ -99,7 +100,7 @@ void mechanism::instantiate(unsigned id, backend::shared_state& shared, const me
     for (auto i: ion_state_tbl) {
         auto ion_binding = value_by_key(overrides.ion_rebind, i.first).value_or(i.first);
 
-        util::optional<ion_state&> oion = value_by_key(shared.ion_data, ion_binding);
+        ion_state* oion = ptr_by_key(shared.ion_data, ion_binding);
         if (!oion) {
             throw arbor_internal_error("multicore/mechanism: mechanism holds ion with no corresponding shared state");
         }
@@ -161,7 +162,7 @@ void mechanism::instantiate(unsigned id, backend::shared_state& shared, const me
     for (auto i: ion_index_table()) {
         auto ion_binding = value_by_key(overrides.ion_rebind, i.first).value_or(i.first);
 
-        util::optional<ion_state&> oion = value_by_key(shared.ion_data, ion_binding);
+        ion_state* oion = ptr_by_key(shared.ion_data, ion_binding);
         if (!oion) {
             throw arbor_internal_error("multicore/mechanism: mechanism holds ion with no corresponding shared state");
         }
diff --git a/arbor/event_binner.cpp b/arbor/event_binner.cpp
index 6301a3499d84d0380a8ba5a7eea72c4db24994a9..84e182a835da7fd080438b2289eefb167f88b78e 100644
--- a/arbor/event_binner.cpp
+++ b/arbor/event_binner.cpp
@@ -1,20 +1,20 @@
 #include <algorithm>
 #include <cmath>
 #include <limits>
+#include <optional>
 #include <stdexcept>
 #include <unordered_map>
 
 #include <arbor/arbexcept.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/spike.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "event_binner.hpp"
 
 namespace arb {
 
 void event_binner::reset() {
-    last_event_time_ = util::nullopt;
+    last_event_time_ = std::nullopt;
 }
 
 time_type event_binner::bin(time_type t, time_type t_min) {
diff --git a/arbor/event_binner.hpp b/arbor/event_binner.hpp
index c1bcd72c5147972a77a258805e26da8ce8a1c8a4..7d12526457dc0365d57f7a93196a376ca4581938 100644
--- a/arbor/event_binner.hpp
+++ b/arbor/event_binner.hpp
@@ -1,11 +1,11 @@
 #pragma once
 
 #include <limits>
+#include <optional>
 #include <unordered_map>
 
 #include <arbor/common_types.hpp>
 #include <arbor/spike.hpp>
-#include <arbor/util/optional.hpp>
 
 namespace arb {
 
@@ -32,7 +32,7 @@ private:
     // Interval in which event times can be aliased.
     time_type bin_interval_;
 
-    util::optional<time_type> last_event_time_;
+    std::optional<time_type> last_event_time_;
 };
 
 } // namespace arb
diff --git a/arbor/event_queue.hpp b/arbor/event_queue.hpp
index b785acd06299d6a3fb60e93d1afa50013e98793c..59885b4361bba861d2328f3e40f568392f0c0d00 100644
--- a/arbor/event_queue.hpp
+++ b/arbor/event_queue.hpp
@@ -2,6 +2,7 @@
 
 #include <cstdint>
 #include <limits>
+#include <optional>
 #include <ostream>
 #include <queue>
 #include <type_traits>
@@ -9,7 +10,6 @@
 
 #include <arbor/common_types.hpp>
 #include <arbor/spike_event.hpp>
-#include <arbor/util/optional.hpp>
 #include <arbor/generic_event.hpp>
 
 namespace arb {
@@ -46,20 +46,20 @@ public:
     }
 
     // Return time t of head of queue if `t_until` > `t`.
-    util::optional<event_time_type> time_if_before(const event_time_type& t_until) {
+    std::optional<event_time_type> time_if_before(const event_time_type& t_until) {
         if (queue_.empty()) {
-            return util::nullopt;
+            return std::nullopt;
         }
 
         using ::arb::event_time;
         auto t = event_time(queue_.top());
-        return t_until > t? util::just(t): util::nullopt;
+        return t_until > t? std::optional(t): std::nullopt;
     }
 
     // Generic conditional pop: pop and return head of queue if
     // queue non-empty and the head satisfies predicate.
     template <typename Pred>
-    util::optional<value_type> pop_if(Pred&& pred) {
+    std::optional<value_type> pop_if(Pred&& pred) {
         using ::arb::event_time;
         if (!queue_.empty() && pred(queue_.top())) {
             auto ev = queue_.top();
@@ -67,12 +67,12 @@ public:
             return ev;
         }
         else {
-            return util::nullopt;
+            return std::nullopt;
         }
     }
 
     // Pop and return top event `ev` of queue if `t_until` > `event_time(ev)`.
-    util::optional<value_type> pop_if_before(const event_time_type& t_until) {
+    std::optional<value_type> pop_if_before(const event_time_type& t_until) {
         using ::arb::event_time;
         return pop_if(
             [&t_until](const value_type& ev) { return t_until > event_time(ev); }
@@ -80,7 +80,7 @@ public:
     }
 
     // Pop and return top event `ev` of queue unless `event_time(ev)` > `t_until`
-    util::optional<value_type> pop_if_not_after(const event_time_type& t_until) {
+    std::optional<value_type> pop_if_not_after(const event_time_type& t_until) {
         using ::arb::event_time;
         return pop_if(
             [&t_until](const value_type& ev) { return !(event_time(ev) > t_until); }
diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index 451002a97c27341fe7279c6ab3fd8d81031ec0bf..539aeeb53b75f700b214e52db1b3cd5969d2f89a 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -1,4 +1,5 @@
 #include <algorithm>
+#include <optional>
 #include <set>
 #include <stdexcept>
 #include <unordered_set>
@@ -10,7 +11,6 @@
 #include <arbor/morph/mcable_map.hpp>
 #include <arbor/morph/mprovider.hpp>
 #include <arbor/morph/morphology.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "fvm_layout.hpp"
 #include "threading/threading.hpp"
@@ -45,7 +45,7 @@ struct get_value {
 };
 
 template <typename V>
-util::optional<V> operator|(const util::optional<V>& a, const util::optional<V>& b) {
+std::optional<V> operator|(const std::optional<V>& a, const std::optional<V>& b) {
     return a? a: b;
 }
 
diff --git a/arbor/fvm_layout.hpp b/arbor/fvm_layout.hpp
index 72221e6bcb23c70d5e7b89d30e52e6197eb9e883..69557bc005414b57d1532908c2beb77296bd4913 100644
--- a/arbor/fvm_layout.hpp
+++ b/arbor/fvm_layout.hpp
@@ -9,7 +9,6 @@
 #include <arbor/mechinfo.hpp>
 #include <arbor/mechcat.hpp>
 #include <arbor/recipe.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "execution_context.hpp"
 #include "util/piecewise.hpp"
diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp
index d36c3bd78f3fb6f16c2824d0344fb0f9ef45236e..26e3a6458db352f499fe6e57d0893cea782ee007 100644
--- a/arbor/fvm_lowered_cell_impl.hpp
+++ b/arbor/fvm_lowered_cell_impl.hpp
@@ -10,6 +10,7 @@
 #include <cmath>
 #include <iterator>
 #include <memory>
+#include <optional>
 #include <queue>
 #include <stdexcept>
 #include <utility>
@@ -21,7 +22,6 @@
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/util/any_visitor.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "builtin_mechanisms.hpp"
 #include "execution_context.hpp"
@@ -697,12 +697,12 @@ struct probe_resolution_data {
     };
 
     // Index into ion data from location.
-    util::optional<fvm_index_type> ion_location_index(const std::string& ion, mlocation loc) const {
+    std::optional<fvm_index_type> ion_location_index(const std::string& ion, mlocation loc) const {
         if (state->ion_data.count(ion)) {
             return util::binary_search_index(M.ions.at(ion).cv,
                 fvm_index_type(D.geometry.location_cv(cell_idx, loc, cv_prefer::cv_nonempty)));
         }
-        return util::nullopt;
+        return std::nullopt;
     }
 };
 
diff --git a/arbor/include/arbor/cable_cell_param.hpp b/arbor/include/arbor/cable_cell_param.hpp
index 6f733ad2a41cec0738ef8a65e215dbdbb0a72e00..958cf2a7de8e4a79bdadb8fddf75b3e90c357c9c 100644
--- a/arbor/include/arbor/cable_cell_param.hpp
+++ b/arbor/include/arbor/cable_cell_param.hpp
@@ -1,14 +1,14 @@
 #pragma once
 
+#include <memory>
+#include <optional>
+#include <unordered_map>
+#include <string>
+
 #include <arbor/arbexcept.hpp>
 #include <arbor/cv_policy.hpp>
 #include <arbor/mechcat.hpp>
 #include <arbor/morph/locset.hpp>
-#include <arbor/util/optional.hpp>
-
-#include <memory>
-#include <unordered_map>
-#include <string>
 
 namespace arb {
 
@@ -26,9 +26,9 @@ struct cable_cell_error: arbor_exception {
 // separately (see below).
 
 struct cable_cell_ion_data {
-    util::optional<double> init_int_concentration;
-    util::optional<double> init_ext_concentration;
-    util::optional<double> init_reversal_potential;
+    std::optional<double> init_int_concentration;
+    std::optional<double> init_ext_concentration;
+    std::optional<double> init_reversal_potential;
 };
 
 // Current clamp description for stimulus specification.
@@ -172,15 +172,15 @@ struct ion_reversal_potential_method {
 // cell defaults can be individually set with `cable_cell:set_default()`.
 
 struct cable_cell_parameter_set {
-    util::optional<double> init_membrane_potential; // [mV]
-    util::optional<double> temperature_K;           // [K]
-    util::optional<double> axial_resistivity;       // [Ω·cm]
-    util::optional<double> membrane_capacitance;    // [F/m²]
+    std::optional<double> init_membrane_potential; // [mV]
+    std::optional<double> temperature_K;           // [K]
+    std::optional<double> axial_resistivity;       // [Ω·cm]
+    std::optional<double> membrane_capacitance;    // [F/m²]
 
     std::unordered_map<std::string, cable_cell_ion_data> ion_data;
     std::unordered_map<std::string, mechanism_desc> reversal_potential_method;
 
-    util::optional<cv_policy> discretization;
+    std::optional<cv_policy> discretization;
 };
 
 extern cable_cell_parameter_set neuron_parameter_defaults;
diff --git a/arbor/include/arbor/mechcat.hpp b/arbor/include/arbor/mechcat.hpp
index 0018791f32818ef657a5ac2df0c74ed805e6ba70..d72394f7111948344f47ef3c45473365e3eecadb 100644
--- a/arbor/include/arbor/mechcat.hpp
+++ b/arbor/include/arbor/mechcat.hpp
@@ -8,7 +8,6 @@
 
 #include <arbor/mechinfo.hpp>
 #include <arbor/mechanism.hpp>
-#include <arbor/util/optional.hpp>
 
 // Mechanism catalogue maintains:
 //
diff --git a/arbor/include/arbor/morph/label_dict.hpp b/arbor/include/arbor/morph/label_dict.hpp
index 750b78710ed044386e0235b3fef3d4d7d9df2146..a68bab2b46d70e6fa68444ba74b873816b3a1924 100644
--- a/arbor/include/arbor/morph/label_dict.hpp
+++ b/arbor/include/arbor/morph/label_dict.hpp
@@ -1,11 +1,11 @@
 #pragma once
 
 #include <memory>
+#include <optional>
 #include <unordered_map>
 
 #include <arbor/morph/locset.hpp>
 #include <arbor/morph/region.hpp>
-#include <arbor/util/optional.hpp>
 
 namespace arb {
 
@@ -21,8 +21,8 @@ public:
     void set(const std::string& name, locset ls);
     void set(const std::string& name, region reg);
 
-    util::optional<const arb::region&> region(const std::string& name) const;
-    util::optional<const arb::locset&> locset(const std::string& name) const;
+    std::optional<arb::region> region(const std::string& name) const;
+    std::optional<arb::locset> locset(const std::string& name) const;
 
     const ps_map& locsets() const;
     const reg_map& regions() const;
diff --git a/arbor/include/arbor/morph/mcable_map.hpp b/arbor/include/arbor/morph/mcable_map.hpp
index 94b638b1e9e73db56b3440a057acca364170efa7..76fae77970873f46ea2c2d85a973b5798d87cb61 100644
--- a/arbor/include/arbor/morph/mcable_map.hpp
+++ b/arbor/include/arbor/morph/mcable_map.hpp
@@ -5,11 +5,11 @@
 // The only mutating operations are insert, emplace, and clear.
 
 #include <algorithm>
+#include <optional>
 #include <vector>
 
 #include <arbor/assert.hpp>
 #include <arbor/morph/primitives.hpp>
-#include <arbor/util/optional.hpp>
 
 namespace arb {
 
@@ -89,7 +89,7 @@ struct mcable_map {
 private:
     std::vector<value_type> elements_;
 
-    util::optional<typename std::vector<value_type>::iterator> insertion_point(const mcable& c) {
+    std::optional<typename std::vector<value_type>::iterator> insertion_point(const mcable& c) {
         struct as_mcable {
             mcable value;
             as_mcable(const value_type& x): value(x.first) {}
@@ -102,13 +102,13 @@ private:
         if (it!=elements_.begin()) {
             mcable prior = std::prev(it)->first;
             if (prior.branch==c.branch && prior.dist_pos>c.prox_pos) {
-                return util::nullopt;
+                return std::nullopt;
             }
         }
         if (it!=elements_.end()) {
             mcable next = it->first;
             if (c.branch==next.branch && c.dist_pos>next.prox_pos) {
-                return util::nullopt;
+                return std::nullopt;
             }
         }
         return it;
diff --git a/arbor/include/arbor/morph/stitch.hpp b/arbor/include/arbor/morph/stitch.hpp
index b90e56dfd0b261145991806745022d5976e17288..d5086642a10dce1fc8a9ccfc31d7df2cc05cb532 100644
--- a/arbor/include/arbor/morph/stitch.hpp
+++ b/arbor/include/arbor/morph/stitch.hpp
@@ -2,6 +2,7 @@
 
 #include <optional>
 #include <memory>
+#include <optional>
 #include <string>
 #include <vector>
 
diff --git a/arbor/include/arbor/simple_sampler.hpp b/arbor/include/arbor/simple_sampler.hpp
index 3e98d21933647e0f6708f0335f545553908b622c..2776a931a27dfe9f6582f940f8edea53c6beb9e4 100644
--- a/arbor/include/arbor/simple_sampler.hpp
+++ b/arbor/include/arbor/simple_sampler.hpp
@@ -12,7 +12,6 @@
 #include <arbor/common_types.hpp>
 #include <arbor/sampling.hpp>
 #include <arbor/util/any_ptr.hpp>
-#include <arbor/util/optional.hpp>
 
 namespace arb {
 
diff --git a/arbor/include/arbor/util/optional.hpp b/arbor/include/arbor/util/optional.hpp
deleted file mode 100644
index 9692167687f309ff5125cdc27e3189591ff6281d..0000000000000000000000000000000000000000
--- a/arbor/include/arbor/util/optional.hpp
+++ /dev/null
@@ -1,434 +0,0 @@
-#pragma once
-
-/* An option class supporting a subset of C++17 std::optional functionality.
- *
- * Difference from C++17 std::optional:
- *
- * Missing functionality (to be added as required):
- *
- *   1. `constexpr` constructors.
- *
- *   2. Comparison operators other than `operator==`.
- *
- *   3. `std::hash` overload.
- *
- *   4. `swap()` method and ADL-available `swap()` function.
- *
- *   5. No `make_optional` function (but see `just` below).
- *
- * Additional/differing functionality:
- *
- *   1. Optional references.
- *
- *      `util::optional<T&>` acts as a value-like wrapper about a possible
- *      reference of type T&. Methods such as `value()` or `value_or()`
- *      return this reference.
- *
- *   2. Optional void.
- *
- *      Included primarily for ease of generic programming with `optional`.
- *
- *   3. `util::just`
- *
- *      This function acts like the value-constructing `std::make_optional<T>(T&&)`,
- *      except that it will return an optional<T&> if given an lvalue T as an argument.
- */
-
-#include <type_traits>
-#include <stdexcept>
-#include <utility>
-
-#include <arbor/util/uninitialized.hpp>
-
-namespace arb {
-namespace util {
-
-template <typename X> struct optional;
-
-struct optional_unset_error: std::runtime_error {
-    explicit optional_unset_error(const std::string& what_str)
-        : std::runtime_error(what_str)
-    {}
-
-    optional_unset_error()
-        : std::runtime_error("optional value unset")
-    {}
-};
-
-struct nullopt_t {};
-constexpr nullopt_t nullopt{};
-
-namespace detail {
-    template <typename Y>
-    struct lift_type {
-        using type = optional<Y>;
-    };
-
-    template <typename Y>
-    struct lift_type<optional<Y>> {
-        using type = optional<Y>;
-    };
-
-    template <typename Y>
-    using lift_type_t = typename lift_type<Y>::type;
-
-    struct optional_tag {};
-
-    template <typename X>
-    using is_optional = std::is_base_of<optional_tag, std::decay_t<X>>;
-
-    template <typename D, typename X>
-    struct wrapped_type_impl {
-        using type = X;
-    };
-
-    template <typename D, typename X>
-    struct wrapped_type_impl<optional<D>, X> {
-        using type = D;
-    };
-
-    template <typename X>
-    struct wrapped_type {
-        using type = typename wrapped_type_impl<std::decay_t<X>, X>::type;
-    };
-
-    template <typename X>
-    using wrapped_type_t = typename wrapped_type<X>::type;
-
-    template <typename X>
-    struct optional_base: detail::optional_tag {
-        template <typename Y> friend struct optional;
-
-    protected:
-        using data_type = util::uninitialized<X>;
-        using rvalue_reference = typename data_type::rvalue_reference;
-        using const_rvalue_reference = typename data_type::const_rvalue_reference;
-
-    public:
-        using value_type = X;
-        using reference = typename data_type::reference;
-        using const_reference = typename data_type::const_reference;
-        using pointer = typename data_type::pointer;
-        using const_pointer = typename data_type::const_pointer;
-
-    protected:
-        bool set;
-        data_type data;
-
-        optional_base() : set(false) {}
-
-        template <typename T>
-        optional_base(bool set_, T&& init) : set(set_) {
-            if (set) {
-                data.construct(std::forward<T>(init));
-            }
-        }
-
-        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(); }
-
-        void assert_set() const {
-            if (!set) {
-                throw optional_unset_error();
-            }
-        }
-
-    public:
-        ~optional_base() {
-            if (set) {
-                data.destruct();
-            }
-        }
-
-        pointer operator->() { return data.ptr(); }
-        const_pointer operator->() const { return data.cptr(); }
-
-        reference operator*() { return ref(); }
-        const_reference operator*() const { return ref(); }
-
-        explicit operator bool() const { return set; }
-
-        template <typename Y>
-        bool operator==(const Y& y) const {
-            return set && ref()==y;
-        }
-
-        template <typename Y>
-        bool operator==(const optional<Y>& o) const {
-            return (set && o.set && ref()==o.ref()) || (!set && !o.set);
-        }
-
-        void reset() {
-            if (set) {
-                data.destruct();
-            }
-            set = false;
-        }
-
-        template <typename Y>
-        void emplace(Y&& y) {
-            if (set) {
-                data.destruct();
-            }
-            data.construct(std::forward<Y>(y));
-            set = true;
-        }
-    };
-
-    // type utilities
-    template <typename T>
-    using enable_unless_optional_t = std::enable_if_t<!is_optional<T>::value>;
-
-    // avoid nonnull address warnings when using operator| with e.g. char array constants
-    template <typename T>
-    bool decay_bool(const T* x) { return static_cast<bool>(x); }
-
-    template <typename T>
-    bool decay_bool(const T& x) { return static_cast<bool>(x); }
-
-} // namespace detail
-
-template <typename X>
-struct optional: detail::optional_base<X> {
-    using base = detail::optional_base<X>;
-    using base::set;
-    using base::ref;
-    using base::reset;
-    using base::data;
-    using base::assert_set;
-
-    optional() noexcept: base() {}
-    optional(nullopt_t) noexcept: base() {}
-
-    optional(const X& x)
-        noexcept(std::is_nothrow_copy_constructible<X>::value): base(true, 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>
-    optional(const optional<T>& ot)
-        noexcept(std::is_nothrow_constructible<X, T>::value): base(ot.set, ot.ref()) {}
-
-    optional(optional&& ot)
-        noexcept(std::is_nothrow_move_constructible<X>::value): base(ot.set, std::move(ot.ref())) {}
-
-    template <typename T>
-    optional(optional<T>&& ot)
-        noexcept(std::is_nothrow_constructible<X, T&&>::value): base(ot.set, std::move(ot.ref())) {}
-
-    optional& operator=(nullopt_t) {
-        reset();
-        return *this;
-    }
-
-    template <
-        typename Y,
-        typename = detail::enable_unless_optional_t<Y>
-    >
-    optional& operator=(Y&& y) {
-        if (set) {
-            ref() = std::forward<Y>(y);
-        }
-        else {
-            set = true;
-            data.construct(std::forward<Y>(y));
-        }
-        return *this;
-    }
-
-    optional& operator=(const optional& o) {
-        if (set) {
-            if (o.set) {
-                ref() = o.ref();
-            }
-            else {
-                reset();
-            }
-        }
-        else {
-            set = o.set;
-            if (set) {
-                data.construct(o.ref());
-            }
-        }
-        return *this;
-    }
-
-    template <
-        typename Y = X,
-        typename = std::enable_if_t<
-            std::is_move_assignable<Y>::value &&
-            std::is_move_constructible<Y>::value
-        >
-    >
-    optional& operator=(optional&& o) {
-        if (set) {
-            if (o.set) {
-                ref() = std::move(o.ref());
-            }
-            else reset();
-        }
-        else {
-            set = o.set;
-            if (set) {
-                data.construct(std::move(o.ref()));
-            }
-        }
-        return *this;
-    }
-
-    X& value() & {
-        return assert_set(), ref();
-    }
-
-    const X& value() const& {
-        return assert_set(), ref();
-    }
-
-    X&& value() && {
-        return assert_set(), std::move(ref());
-    }
-
-    const X&& value() const&& {
-        return assert_set(), std::move(ref());
-    }
-
-    template <typename T>
-    X value_or(T&& alternative) const& {
-        return set? value(): static_cast<X>(std::forward<T>(alternative));
-    }
-
-    template <typename T>
-    X value_or(T&& alternative) && {
-        return set? std::move(value()): static_cast<X>(std::forward<T>(alternative));
-    }
-};
-
-template <typename X>
-struct optional<X&>: detail::optional_base<X&> {
-    using base=detail::optional_base<X&>;
-    using base::set;
-    using base::ref;
-    using base::data;
-    using base::reset;
-    using base::assert_set;
-
-    optional() noexcept: base() {}
-    optional(nullopt_t) noexcept: base() {}
-    optional(X& x) noexcept: base(true, x) {}
-
-    template <typename T>
-    optional(optional<T&>& ot) noexcept: base(ot.set, ot.ref()) {}
-
-    optional& operator=(nullopt_t) {
-        reset();
-        return *this;
-    }
-
-    template <typename Y>
-    optional& operator=(Y& y) {
-        set = true;
-        data.construct(y);
-        return *this;
-    }
-
-    template <typename Y>
-    optional& operator=(optional<Y&>& o) {
-        set = o.set;
-        if (o.set) {
-           data.construct(o.value());
-        }
-        return *this;
-    }
-
-    X& value() {
-        return assert_set(), ref();
-    }
-
-    const X& value() const {
-        return assert_set(), ref();
-    }
-
-    X& value_or(X& alternative) & {
-        return set? ref(): alternative;
-    }
-
-    const X& value_or(const X& alternative) const& {
-        return set? ref(): alternative;
-    }
-
-    template <typename T>
-    const X value_or(const T& alternative) && {
-        return set? ref(): static_cast<X>(alternative);
-    }
-};
-
-template <>
-struct optional<void>: detail::optional_base<void> {
-    using base = detail::optional_base<void>;
-    using base::assert_set;
-    using base::set;
-    using base::reset;
-
-    optional(): base() {}
-    optional(nullopt_t): base() {}
-
-    template <typename T>
-    optional(T): base(true, true) {}
-
-    template <typename T>
-    optional(const optional<T>& o): base(o.set, true) {}
-
-    optional& operator=(nullopt_t) {
-        reset();
-        return *this;
-    }
-
-    template <typename T>
-    optional& operator=(T) {
-        set = true;
-        return *this;
-    }
-
-    template <typename T>
-    optional& operator=(const optional<T>& o) {
-        set = o.set;
-        return *this;
-    }
-
-    template <typename Y>
-    bool operator==(const Y& y) const { return false; }
-
-    bool operator==(const optional<void>& o) const {
-        return (set && o.set) || (!set && !o.set);
-    }
-
-    void value() const { assert_set(); }
-
-    template <typename T>
-    void value_or(T) const {} // nop
-};
-
-template <typename X>
-optional<X> just(X&& x) {
-    return optional<X>(std::forward<X>(x));
-}
-
-} // namespace util
-} // namespace arb
diff --git a/arbor/mc_cell_group.cpp b/arbor/mc_cell_group.cpp
index 124734bec01cf29e6ad13bba9c0fd09289ae1ac4..ba77e032e51673045b3bb972d00aee2e5979b62e 100644
--- a/arbor/mc_cell_group.cpp
+++ b/arbor/mc_cell_group.cpp
@@ -1,4 +1,5 @@
 #include <functional>
+#include <optional>
 #include <unordered_set>
 #include <variant>
 #include <vector>
@@ -506,7 +507,7 @@ void mc_cell_group::remove_all_samplers() {
 std::vector<probe_metadata> mc_cell_group::get_probe_metadata(cell_member_type probe_id) const {
     // Probe associations are fixed after construction, so we do not need to grab the mutex.
 
-    util::optional<probe_tag> maybe_tag = util::value_by_key(probe_map_.tag, probe_id);
+    std::optional<probe_tag> maybe_tag = util::value_by_key(probe_map_.tag, probe_id);
     if (!maybe_tag) {
         return {};
     }
diff --git a/arbor/mechcat.cpp b/arbor/mechcat.cpp
index ab30845c3ee47a5aa049b0c96f9ca64eed8ac174..3fac9e805dc2aeabe7d479d6682c5ce57ab56ccc 100644
--- a/arbor/mechcat.cpp
+++ b/arbor/mechcat.cpp
@@ -59,7 +59,7 @@
 
 namespace arb {
 
-using util::value_by_key;
+using util::ptr_by_key;
 using util::unexpected;
 
 using std::make_unique;
@@ -209,10 +209,10 @@ struct catalogue_state {
     // Retrieve mechanism info for mechanism, derived mechanism, or implicitly
     // derived mechanism.
     hopefully<mechanism_info> info(const std::string& name) const {
-        if (const auto& deriv = value_by_key(derived_map_, name)) {
+        if (const auto* deriv = ptr_by_key(derived_map_, name)) {
             return *(deriv->derived_info.get());
         }
-        else if (auto p = value_by_key(info_map_, name)) {
+        else if (auto* p = ptr_by_key(info_map_, name)) {
             return *(p->get());
         }
         else if (auto deriv = derive(name)) {
@@ -238,12 +238,12 @@ struct catalogue_state {
             }
         }
 
-        while (auto maybe_deriv = value_by_key(derived_map_, *base)) {
-            base = &maybe_deriv->parent;
+        while (auto* deriv = ptr_by_key(derived_map_, *base)) {
+            base = &deriv->parent;
         }
 
-        if (const auto& p = value_by_key(info_map_, *base)) {
-            return &p.value()->fingerprint;
+        if (const auto* p = ptr_by_key(info_map_, *base)) {
+            return &p->get()->fingerprint;
         }
 
         throw arbor_internal_error("inconsistent catalogue map state");
@@ -279,7 +279,7 @@ struct catalogue_state {
             const auto& param = kv.first;
             const auto& value = kv.second;
 
-            if (auto p = value_by_key(new_info->globals, param)) {
+            if (auto* p = ptr_by_key(new_info->globals, param)) {
                 if (!p->valid(value)) {
                     return unexpected_exception_ptr(invalid_parameter_value(name, param, value));
                 }
@@ -302,7 +302,7 @@ struct catalogue_state {
 
         string_map<ion_dependency> new_ions;
         for (const auto& kv: new_info->ions) {
-            if (auto new_ion = value_by_key(ion_remap_map, kv.first)) {
+            if (auto* new_ion = ptr_by_key(ion_remap_map, kv.first)) {
                 if (!new_ions.insert({*new_ion, kv.second}).second) {
                     return unexpected_exception_ptr(invalid_ion_remap(name, kv.first, *new_ion));
                 }
@@ -408,14 +408,14 @@ struct catalogue_state {
         }
 
         for (;;) {
-            if (const auto mech_impls = value_by_key(impl_map_, *impl_name)) {
-                if (auto p = value_by_key(mech_impls.value(), tidx)) {
+            if (const auto* mech_impls = ptr_by_key(impl_map_, *impl_name)) {
+                if (auto* p = ptr_by_key(*mech_impls, tidx)) {
                     return p->get()->clone();
                 }
             }
 
             // Try parent instead.
-            if (const auto p = value_by_key(derived_map_, *impl_name)) {
+            if (const auto* p = ptr_by_key(derived_map_, *impl_name)) {
                 impl_name = &p->parent;
             }
             else {
@@ -436,13 +436,13 @@ struct catalogue_state {
             if (!deriv.ion_remap.empty()) {
                 string_map<std::string> new_rebind = deriv.ion_remap;
                 for (auto& kv: over.ion_rebind) {
-                    if (auto opt_v = value_by_key(deriv.ion_remap, kv.second)) {
+                    if (auto* v = ptr_by_key(deriv.ion_remap, kv.second)) {
                         new_rebind.erase(kv.second);
-                        new_rebind[kv.first] = *opt_v;
+                        new_rebind[kv.first] = *v;
                     }
                 }
                 for (auto& kv: over.ion_rebind) {
-                    if (!value_by_key(deriv.ion_remap, kv.second)) {
+                    if (!ptr_by_key(deriv.ion_remap, kv.second)) {
                         new_rebind[kv.first] = kv.second;
                     }
                 }
@@ -455,13 +455,13 @@ struct catalogue_state {
         // requested mechanism.
 
         auto apply_globals = [this, &apply_deriv](auto& self, const std::string& name, mechanism_overrides& over) -> void {
-            if (auto p = value_by_key(derived_map_, name)) {
+            if (auto* p = ptr_by_key(derived_map_, name)) {
                 self(self, p->parent, over);
                 apply_deriv(over, *p);
             }
         };
 
-        util::optional<derivation> implicit_deriv;
+        std::optional<derivation> implicit_deriv;
         if (!defined(name)) {
             if (auto deriv = derive(name)) {
                 implicit_deriv = std::move(deriv.value());
diff --git a/arbor/morph/label_dict.cpp b/arbor/morph/label_dict.cpp
index 0d15b567ba21a1aaacf96a86e72c8d68f6d72d2d..9c514604a16c11159b963828d8303138ef3b67f1 100644
--- a/arbor/morph/label_dict.cpp
+++ b/arbor/morph/label_dict.cpp
@@ -1,4 +1,6 @@
+#include <optional>
 #include <unordered_map>
+#include <utility>
 
 #include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/label_dict.hpp>
@@ -34,13 +36,13 @@ void label_dict::import(const label_dict& other, const std::string& prefix) {
     }
 }
 
-util::optional<const region&> label_dict::region(const std::string& name) const {
+std::optional<region> label_dict::region(const std::string& name) const {
     auto it = regions_.find(name);
     if (it==regions_.end()) return {};
     return it->second;
 }
 
-util::optional<const locset&> label_dict::locset(const std::string& name) const {
+std::optional<locset> label_dict::locset(const std::string& name) const {
     auto it = locsets_.find(name);
     if (it==locsets_.end()) return {};
     return it->second;
diff --git a/arbor/morph/region.cpp b/arbor/morph/region.cpp
index 895666dcfacf4d367796af2b6699c35b6a4416b1..5e7c8a5afa243a49d0d9d56e0b5d7716b67399bd 100644
--- a/arbor/morph/region.cpp
+++ b/arbor/morph/region.cpp
@@ -1,4 +1,4 @@
-#include <any>
+#include <optional>
 #include <stack>
 #include <string>
 #include <vector>
@@ -9,7 +9,6 @@
 #include <arbor/morph/morphexcept.hpp>
 #include <arbor/morph/mprovider.hpp>
 #include <arbor/morph/region.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "s_expr.hpp"
 #include "util/mergeview.hpp"
@@ -21,12 +20,12 @@
 namespace arb {
 namespace reg {
 
-util::optional<mcable> intersect(const mcable& a, const mcable& b) {
-    if (a.branch!=b.branch) return util::nullopt;
+std::optional<mcable> intersect(const mcable& a, const mcable& b) {
+    if (a.branch!=b.branch) return std::nullopt;
 
     double prox = std::max(a.prox_pos, b.prox_pos);
     double dist = std::min(a.dist_pos, b.dist_pos);
-    return prox<=dist? util::just(mcable{a.branch, prox, dist}): util::nullopt;
+    return prox<=dist? std::optional(mcable{a.branch, prox, dist}): std::nullopt;
 }
 
 // Empty region.
diff --git a/arbor/util/hostname.cpp b/arbor/util/hostname.cpp
index 7b335de98d85bbca787e3a42f0e030cc68206ec1..754a478516e61e553a5c037412443a3cdce6f743 100644
--- a/arbor/util/hostname.cpp
+++ b/arbor/util/hostname.cpp
@@ -1,7 +1,6 @@
+#include <optional>
 #include <string>
 
-#include <arbor/util/optional.hpp>
-
 #include "hostname.hpp"
 
 #ifdef __linux__
@@ -14,19 +13,19 @@ namespace arb {
 namespace util {
 
 #ifdef __linux__
-util::optional<std::string> hostname() {
+std::optional<std::string> hostname() {
     // Hostnames can be up to 256 characters in length, however on many systems
     // it is limitted to 64.
     char name[256];
     auto result = gethostname(name, sizeof(name));
     if (result) {
-        return util::nullopt;
+        return std::nullopt;
     }
     return std::string(name);
 }
 #else
-util::optional<std::string> hostname() {
-    return util::nullopt;
+std::optional<std::string> hostname() {
+    return std::nullopt;
 }
 #endif
 
diff --git a/arbor/util/hostname.hpp b/arbor/util/hostname.hpp
index 4eaaf7f72e026f41d268b59223cd996d26dc928e..1108fce0c213288c644632643a9114b0735da77c 100644
--- a/arbor/util/hostname.hpp
+++ b/arbor/util/hostname.hpp
@@ -1,14 +1,13 @@
 #pragma once
 
+#include <optional>
 #include <string>
 
-#include <arbor/util/optional.hpp>
-
 namespace arb {
 namespace util {
 
 // Get the name of the host on which this process is running.
-util::optional<std::string> hostname();
+std::optional<std::string> hostname();
 
 } // namespace util
 } // namespace arb
diff --git a/arbor/util/maputil.hpp b/arbor/util/maputil.hpp
index bcecd481c8e28f64d1d41a510ae14245b389ad3e..693d91454b55104d3b242049a44bc24cbee88f37 100644
--- a/arbor/util/maputil.hpp
+++ b/arbor/util/maputil.hpp
@@ -3,11 +3,10 @@
 #include <algorithm>
 #include <cstring>
 #include <iterator>
+#include <optional>
 #include <utility>
 #include <type_traits>
 
-#include <arbor/util/optional.hpp>
-
 #include "util/meta.hpp"
 #include "util/transform.hpp"
 
@@ -61,20 +60,30 @@ namespace maputil_impl {
         typename Seq,
         typename Key,
         typename Eq = std::equal_to<>,
-        typename Ret0 = decltype(get<1>(*begin(std::declval<Seq&&>()))),
-        typename Ret = std::conditional_t<
-            std::is_rvalue_reference<Seq&&>::value || !std::is_lvalue_reference<Ret0>::value,
-            std::remove_reference_t<Ret0>,
-            Ret0
-        >
+        typename Ret = std::remove_reference_t<decltype(get<1>(*begin(std::declval<Seq&&>())))>
     >
-    optional<Ret> value_by_key(std::false_type, Seq&& seq, const Key& key, Eq eq=Eq{}) {
+    std::optional<Ret> value_by_key(std::false_type, Seq&& seq, const Key& key, Eq eq=Eq{}) {
         for (auto&& entry: seq) {
             if (eq(get<0>(entry), key)) {
                 return get<1>(entry);
             }
         }
-        return nullopt;
+        return std::nullopt;
+    }
+
+    template <
+        typename Seq,
+        typename Key,
+        typename Eq = std::equal_to<>,
+        typename Ret = std::remove_reference_t<decltype(get<1>(*begin(std::declval<Seq&&>())))>
+    >
+    Ret* ptr_by_key(std::false_type, Seq&& seq, const Key& key, Eq eq=Eq{}) {
+        for (auto&& entry: seq) {
+            if (eq(get<0>(entry), key)) {
+                return &get<1>(entry);
+            }
+        }
+        return nullptr;
     }
 
     // use map find
@@ -82,22 +91,30 @@ namespace maputil_impl {
         typename Assoc,
         typename Key,
         typename FindRet = decltype(std::declval<Assoc&&>().find(std::declval<Key>())),
-        typename Ret0 = decltype(get<1>(*std::declval<FindRet>())),
-        typename Ret = std::conditional_t<
-            std::is_rvalue_reference<Assoc&&>::value || !std::is_lvalue_reference<Ret0>::value,
-            std::remove_reference_t<Ret0>,
-            Ret0
-        >
+        typename Ret = std::remove_reference_t<decltype(get<1>(*std::declval<FindRet>()))>
     >
-    optional<Ret> value_by_key(std::true_type, Assoc&& map, const Key& key) {
+    std::optional<Ret> value_by_key(std::true_type, Assoc&& map, const Key& key) {
         auto it = map.find(key);
         if (it!=std::end(map)) {
             return get<1>(*it);
         }
-        return nullopt;
+        return std::nullopt;
+    }
+
+    template <
+        typename Assoc,
+        typename Key,
+        typename FindRet = decltype(std::declval<Assoc&&>().find(std::declval<Key>())),
+        typename Ret = std::remove_reference_t<decltype(get<1>(*std::declval<FindRet>()))>
+    >
+    Ret* ptr_by_key(std::true_type, Assoc&& map, const Key& key) {
+        auto it = map.find(key);
+        return it!=std::end(map)? &get<1>(*it): nullptr;
     }
 }
 
+// Return copy of value associated with key, wrapped in std::optional, or std::nullopty.
+
 template <typename C, typename Key, typename Eq>
 auto value_by_key(C&& c, const Key& k, Eq eq) {
     return maputil_impl::value_by_key(std::false_type{}, std::forward<C>(c), k, eq);
@@ -110,15 +127,29 @@ auto value_by_key(C&& c, const Key& k) {
         std::forward<C>(c), k);
 }
 
+// Return pointer to value associated with key, or nullptr.
+
+template <typename C, typename Key, typename Eq>
+auto ptr_by_key(C&& c, const Key& k, Eq eq) {
+    return maputil_impl::ptr_by_key(std::false_type{}, std::forward<C>(c), k, eq);
+}
+
+template <typename C, typename Key>
+auto ptr_by_key(C&& c, const Key& k) {
+    return maputil_impl::ptr_by_key(
+        std::integral_constant<bool, is_associative_container<C>::value>{},
+        std::forward<C>(c), k);
+}
+
 // Find the index into an ordered sequence of a value by binary search;
 // returns optional<size_type> for the size_type associated with the sequence.
 // (Note: this is pretty much all we use algorthim::binary_find for.) 
 
 template <typename C, typename Key>
-optional<typename sequence_traits<C>::difference_type> binary_search_index(const C& c, const Key& key) {
+std::optional<typename sequence_traits<C>::difference_type> binary_search_index(const C& c, const Key& key) {
     auto strict = strict_view(c);
     auto it = std::lower_bound(strict.begin(), strict.end(), key);
-    return it!=strict.end() && key==*it? just(std::distance(strict.begin(), it)): nullopt;
+    return it!=strict.end() && key==*it? std::optional(std::distance(strict.begin(), it)): std::nullopt;
 }
 
 // As binary_search_index above, but compare key against the proj(x) for elements
@@ -126,11 +157,11 @@ optional<typename sequence_traits<C>::difference_type> binary_search_index(const
 // increasing.
 
 template <typename C, typename Key, typename Proj>
-optional<typename sequence_traits<C>::difference_type> binary_search_index(const C& c, const Key& key, const Proj& proj) {
+std::optional<typename sequence_traits<C>::difference_type> binary_search_index(const C& c, const Key& key, const Proj& proj) {
     auto strict = strict_view(c);
     auto projected = transform_view(strict, proj);
     auto it = std::lower_bound(projected.begin(), projected.end(), key);
-    return it!=strict.end() && key==*it? just(std::distance(projected.begin(), it)): nullopt;
+    return it!=strict.end() && key==*it? std::optional(std::distance(projected.begin(), it)): std::nullopt;
 }
 
 
diff --git a/arborenv/gpu_uuid.cpp b/arborenv/gpu_uuid.cpp
index 3d2c55d074510fe36b4eae339f102892b8751ae5..fe09cfbe57c5ce5e6ccc871fa85bca198fa32cfd 100644
--- a/arborenv/gpu_uuid.cpp
+++ b/arborenv/gpu_uuid.cpp
@@ -5,11 +5,11 @@
 #include <iomanip>
 #include <ios>
 #include <numeric>
+#include <optional>
 #include <ostream>
 #include <stdexcept>
 #include <vector>
 
-#include <arbor/util/optional.hpp>
 #include <arbor/util/scope_exit.hpp>
 #include "gpu_uuid.hpp"
 #include "gpu_api.hpp"
@@ -20,19 +20,19 @@ extern "C" {
     #include <unistd.h>
 }
 
-arb::util::optional<std::string> get_hostname() {
+std::optional<std::string> get_hostname() {
     // Hostnames can be up to 256 characters in length, however on many systems
     // it is limitted to 64.
     char name[256];
     auto result = gethostname(name, sizeof(name));
     if (result) {
-        return arb::util::nullopt;
+        return std::nullopt;
     }
     return std::string(name);
 }
 #else
-arb::util::optional<std::string> get_hostname() {
-    return arb::util::nullopt;
+std::optional<std::string> get_hostname() {
+    return std::nullopt;
 }
 #endif
 
diff --git a/example/brunel/brunel.cpp b/example/brunel/brunel.cpp
index 36c74da9cb8a31f805874afa9d69e23057809e75..03f63e8e1e2cb7f5896790624879de2471b3034b 100644
--- a/example/brunel/brunel.cpp
+++ b/example/brunel/brunel.cpp
@@ -4,6 +4,7 @@
 #include <iomanip>
 #include <iostream>
 #include <memory>
+#include <optional>
 #include <set>
 #include <vector>
 
@@ -19,7 +20,6 @@
 #include <arbor/profile/profiler.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/simulation.hpp>
-#include <arbor/util/optional.hpp>
 #include <arbor/version.hpp>
 
 #include <arborenv/concurrency.hpp>
@@ -68,7 +68,7 @@ struct cl_options {
 
 std::ostream& operator<<(std::ostream& o, const cl_options& opt);
 
-util::optional<cl_options> read_options(int argc, char** argv);
+std::optional<cl_options> read_options(int argc, char** argv);
 
 void banner(const context& ctx);
 
@@ -346,7 +346,7 @@ std::vector<cell_gid_type> sample_subset(cell_gid_type gid, cell_gid_type start,
 }
 
 // Read options from (optional) json file and command line arguments.
-util::optional<cl_options> read_options(int argc, char** argv) {
+std::optional<cl_options> read_options(int argc, char** argv) {
     using namespace to;
     auto usage_str = "\n"
                      "-n|--n-excitatory      [Number of cells in the excitatory population]\n"
diff --git a/python/cells.cpp b/python/cells.cpp
index 54510c9070d5d3a359967fa45492ae3733f880a1..2ac158fcda90262a95b7c4a910c433a45864d222 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -21,7 +21,6 @@
 #include <arbor/schedule.hpp>
 #include <arbor/spike_source_cell.hpp>
 #include <arbor/util/any_cast.hpp>
-#include <arbor/util/optional.hpp>
 #include <arbor/util/unique_any.hpp>
 
 #include "cells.hpp"
@@ -215,7 +214,7 @@ std::string mechanism_desc_str(const arb::mechanism_desc& md) {
 
 void register_cells(pybind11::module& m) {
     using namespace pybind11::literals;
-    using arb::util::optional;
+    using std::optional;
 
     // arb::spike_source_cell
 
diff --git a/python/context.cpp b/python/context.cpp
index bd70f1512030b0cf986b2d029763077e3aa96a40..e8cd4dbcb61a0cd5365f5d69c2c1f45f13ce234b 100644
--- a/python/context.cpp
+++ b/python/context.cpp
@@ -1,4 +1,5 @@
 #include <iostream>
+#include <optional>
 #include <sstream>
 #include <string>
 
@@ -6,7 +7,6 @@
 
 #include <arbor/context.hpp>
 #include <arbor/version.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "context.hpp"
 #include "conversion.hpp"
@@ -33,7 +33,7 @@ std::ostream& operator<<(std::ostream& o, const context_shim& ctx) {
 
 // A Python shim that holds the information that describes an arb::proc_allocation.
 struct proc_allocation_shim {
-    arb::util::optional<int> gpu_id = {};
+    std::optional<int> gpu_id = {};
     int num_threads = 1;
 
     proc_allocation_shim(int threads, pybind11::object gpu) {
@@ -53,7 +53,7 @@ struct proc_allocation_shim {
         num_threads = threads;
     };
 
-    arb::util::optional<int> get_gpu_id() const { return gpu_id; }
+    std::optional<int> get_gpu_id() const { return gpu_id; }
     int get_num_threads() const { return num_threads; }
     bool has_gpu() const { return bool(gpu_id); }
 
@@ -64,7 +64,7 @@ struct proc_allocation_shim {
 };
 
 std::ostream& operator<<(std::ostream& o, const proc_allocation_shim& alloc) {
-    return o << "<arbor.proc_allocation: threads " << alloc.num_threads << ", gpu_id " << alloc.gpu_id << ">";
+    return o << "<arbor.proc_allocation: threads " << alloc.num_threads << ", gpu_id " << util::to_string(alloc.gpu_id) << ">";
 }
 
 void register_contexts(pybind11::module& m) {
diff --git a/python/conversion.hpp b/python/conversion.hpp
index eda94ba044a36997dbe6b58dc1a303fdfe4c985d..30b6c11aa851ad81f93860f5e357ec690abfe8bc 100644
--- a/python/conversion.hpp
+++ b/python/conversion.hpp
@@ -1,19 +1,13 @@
 #pragma once
 
+#include <optional>
+
 #include <pybind11/pybind11.h>
 #include <pybind11/pytypes.h>
 #include <pybind11/stl.h>
 
-#include <arbor/util/optional.hpp>
-
 #include "error.hpp"
 
-// from https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html?highlight=boost%3A%3Aoptional#c-17-library-containers
-namespace pybind11 { namespace detail {
-    template <typename T>
-    struct type_caster<arb::util::optional<T>>: optional_caster<arb::util::optional<T>> {};
-}}
-
 namespace pyarb {
 
 struct is_nonneg {
@@ -35,7 +29,7 @@ struct is_positive {
 // Throws an runtime_error exception with msg if either the Python object
 // can't be converted to type T, or if the predicate is false for the value.
 template <typename T, typename F>
-arb::util::optional<T> py2optional(pybind11::object o, const char* msg, F&& pred) {
+std::optional<T> py2optional(pybind11::object o, const char* msg, F&& pred) {
     bool ok = true;
     T value;
 
@@ -53,11 +47,11 @@ arb::util::optional<T> py2optional(pybind11::object o, const char* msg, F&& pred
         throw pyarb_error(msg);
     }
 
-    return o.is_none()? arb::util::nullopt: arb::util::optional<T>(std::move(value));
+    return o.is_none()? std::nullopt: std::optional<T>(std::move(value));
 }
 
 template <typename T>
-arb::util::optional<T> py2optional(pybind11::object o, const char* msg) {
+std::optional<T> py2optional(pybind11::object o, const char* msg) {
     T value;
 
     if (!o.is_none()) {
@@ -69,7 +63,7 @@ arb::util::optional<T> py2optional(pybind11::object o, const char* msg) {
         }
     }
 
-    return o.is_none()? arb::util::nullopt: arb::util::optional<T>(std::move(value));
+    return o.is_none()? std::nullopt: std::optional<T>(std::move(value));
 }
 
 // Attempt to cast a Python object to a C++ type T.
@@ -77,8 +71,8 @@ arb::util::optional<T> py2optional(pybind11::object o, const char* msg) {
 // to T, otherwise it is empty. Hence not being able
 // to cast is not an error.
 template <typename T>
-arb::util::optional<T> try_cast(pybind11::object o) {
-    if (o.is_none()) return arb::util::nullopt;
+std::optional<T> try_cast(pybind11::object o) {
+    if (o.is_none()) return std::nullopt;
 
     try {
         return o.cast<T>();
@@ -86,7 +80,7 @@ arb::util::optional<T> try_cast(pybind11::object o) {
     // Ignore cast_error: if unable to perform cast.
     catch (pybind11::cast_error& e) {}
 
-    return arb::util::nullopt;
+    return std::nullopt;
 }
 
 } // namespace arb
diff --git a/python/event_generator.cpp b/python/event_generator.cpp
index ceb5ec53dc54dc60e1ba52e8c84cde2d96675f44..957e75e2d56e8ceea5d73889e5358035f025953a 100644
--- a/python/event_generator.cpp
+++ b/python/event_generator.cpp
@@ -1,17 +1,10 @@
-#include <stdexcept>
-#include <sstream>
-#include <string>
-
 #include <pybind11/pybind11.h>
 #include <pybind11/pytypes.h>
 #include <pybind11/stl.h>
 
 #include <arbor/common_types.hpp>
 #include <arbor/schedule.hpp>
-#include <arbor/util/optional.hpp>
 
-#include "conversion.hpp"
-#include "error.hpp"
 #include "event_generator.hpp"
 #include "schedule.hpp"
 
diff --git a/python/mechanism.cpp b/python/mechanism.cpp
index c710c013f12a966473bba6075828480da13c0b20..09356d66ce84130c9722252c4c2f4272a97bdb96 100644
--- a/python/mechanism.cpp
+++ b/python/mechanism.cpp
@@ -1,3 +1,6 @@
+#include <optional>
+#include <stdexcept>
+
 #include <pybind11/pybind11.h>
 #include "pybind11/pytypes.h"
 #include <pybind11/stl.h>
@@ -5,8 +8,6 @@
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/mechanism.hpp>
 #include <arbor/mechcat.hpp>
-#include <arbor/util/optional.hpp>
-#include <stdexcept>
 
 #include "arbor/mechinfo.hpp"
 
@@ -37,7 +38,7 @@ void apply_derive(arb::mechanism_catalogue& m,
 }
 
 void register_mechanisms(pybind11::module& m) {
-    using arb::util::optional;
+    using std::optional;
     using namespace pybind11::literals;
 
     pybind11::class_<arb::mechanism_field_spec> field_spec(m, "mechanism_field",
diff --git a/python/schedule.cpp b/python/schedule.cpp
index f186fe0c2881d0fc206cabc7a05eac6d30ce6186..ff51408b63ae719f4467191a9d70925ab5001524 100644
--- a/python/schedule.cpp
+++ b/python/schedule.cpp
@@ -1,6 +1,5 @@
 #include <arbor/schedule.hpp>
 #include <arbor/common_types.hpp>
-#include <arbor/util/optional.hpp>
 
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
@@ -13,9 +12,9 @@ namespace pyarb {
 
 std::ostream& operator<<(std::ostream& o, const regular_schedule_shim& x) {
     return o << "<arbor.regular_schedule: tstart "
-             << x.tstart << " ms, dt "
+             << util::to_string(x.tstart) << " ms, dt "
              << x.dt << " ms, tstop "
-             << x.tstop << " ms>";
+             << util::to_string(x.tstop) << " ms>";
 }
 
 std::ostream& operator<<(std::ostream& o, const explicit_schedule_shim& e) {
diff --git a/python/schedule.hpp b/python/schedule.hpp
index 072160713bb0e15a2b8c693b85b9d50ebf0b1a00..5aefeb5fcbc5e783e0764d01688469f2cdf23197 100644
--- a/python/schedule.hpp
+++ b/python/schedule.hpp
@@ -1,11 +1,14 @@
 #pragma once
 
+#include <optional>
+#include <random>
+#include <vector>
+
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
 #include <arbor/schedule.hpp>
 #include <arbor/common_types.hpp>
-#include <arbor/util/optional.hpp>
 
 namespace pyarb {
 
@@ -15,7 +18,7 @@ namespace pyarb {
 // an arb::regular_schedule when a C++ recipe is created from a Python recipe.
 struct regular_schedule_shim {
     using time_type = arb::time_type;
-    using opt_time_type = arb::util::optional<time_type>;
+    using opt_time_type = std::optional<time_type>;
 
     opt_time_type tstart = {};
     opt_time_type tstop = {};
diff --git a/python/strprintf.hpp b/python/strprintf.hpp
index 5e2699bd41d3a91194d6107cf4d0b0e054eb11f7..638016772661773b093a20fbde94b9b2046c181c 100644
--- a/python/strprintf.hpp
+++ b/python/strprintf.hpp
@@ -4,18 +4,44 @@
 
 #include <cstdio>
 #include <memory>
+#include <optional>
 #include <string>
 #include <sstream>
 #include <system_error>
+#include <type_traits>
 #include <unordered_map>
 #include <utility>
 #include <vector>
 
-#include <arbor/util/optional.hpp>
-
 namespace pyarb {
 namespace util {
 
+namespace impl {
+    // Wrapper for formatted output of optional values.
+    template <typename T>
+    struct opt_wrap {
+        const T& ref;
+        opt_wrap(const T& ref): ref(ref) {}
+        friend std::ostream& operator<<(std::ostream& out, const opt_wrap& wrap) {
+            return out << wrap.ref;
+        }
+    };
+
+    template <typename T>
+    struct opt_wrap<std::optional<T>> {
+        const std::optional<T>& ref;
+        opt_wrap(const std::optional<T>& ref): ref(ref) {}
+        friend std::ostream& operator<<(std::ostream& out, const opt_wrap& wrap) {
+            if (wrap.ref) {
+                return out << *wrap.ref;
+            }
+            else {
+                return out << "None";
+            }
+        }
+    };
+}
+
 // Use ADL to_string or std::to_string, falling back to ostream formatting:
 
 namespace impl_to_string {
@@ -25,17 +51,13 @@ namespace impl_to_string {
     struct select {
         static std::string str(const T& value) {
             std::ostringstream o;
-            o << value;
+            o << impl::opt_wrap(value);
             return o.str();
         }
     };
 
-    // Can be eplaced with std::void_t in c++17.
-    template <typename ...Args>
-    using void_t = void;
-
     template <typename T>
-    struct select<T, void_t<decltype(to_string(std::declval<T>()))>> {
+    struct select<T, std::void_t<decltype(to_string(std::declval<T>()))>> {
         static std::string str(const T& v) {
             return to_string(v);
         }
@@ -100,7 +122,7 @@ namespace impl {
         }
         o.write(s, t-s);
         if (*t) {
-            o << std::forward<T>(value);
+            o << opt_wrap{value};
             pprintf_(o, t+2, std::forward<Tail>(tail)...);
         }
     }
@@ -217,14 +239,4 @@ std::string dictionary_csv(const std::unordered_map<Key, T>& dict) {
 }
 
 } // namespace util
-
-}
-
-namespace arb {
-namespace util {
-template <typename T>
-std::ostream& operator<<(std::ostream& o, const arb::util::optional<T>& x) {
-    return o << (x? pyarb::util::to_string(*x): "None");
-}
-}
-}
+} // namespace pyarb
diff --git a/sup/include/sup/json_params.hpp b/sup/include/sup/json_params.hpp
index fdcf0a20e2be4df7bffa7ec8ca06a5b457743c33..05ce805d1b4a0ca761ed9fd715e18f9dd0f07d71 100644
--- a/sup/include/sup/json_params.hpp
+++ b/sup/include/sup/json_params.hpp
@@ -2,8 +2,7 @@
 
 #include <array>
 #include <exception>
-
-#include <arbor/util/optional.hpp>
+#include <optional>
 
 #include <nlohmann/json.hpp>
 
@@ -12,10 +11,10 @@ namespace sup {
 // Search a json object for an entry with a given name.
 // If found, return the value and remove from json object.
 template <typename T>
-arb::util::optional<T> find_and_remove_json(const char* name, nlohmann::json& j) {
+std::optional<T> find_and_remove_json(const char* name, nlohmann::json& j) {
     auto it = j.find(name);
     if (it==j.end()) {
-        return arb::util::nullopt;
+        return std::nullopt;
     }
     T value = std::move(*it);
     j.erase(name);
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 7bf826b14ec94a8f10a1af85325f653cf48eb12b..eb3ef8d6476ae54cc0b3bcd512c037bb1502dd72 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -128,7 +128,6 @@ set(unit_sources
     test_morph_primitives.cpp
     test_morph_stitch.cpp
     test_multi_event_stream.cpp
-    test_optional.cpp
     test_ordered_forest.cpp
     test_padded.cpp
     test_partition.cpp
diff --git a/test/unit/test_cv_geom.cpp b/test/unit/test_cv_geom.cpp
index a007a86b7277905b06a4b9ed8abae7c01f7302eb..8e883ab0809037cf63baaf328c0e10bf0ec027bd 100644
--- a/test/unit/test_cv_geom.cpp
+++ b/test/unit/test_cv_geom.cpp
@@ -1,7 +1,6 @@
 #include <algorithm>
 #include <utility>
 
-#include <arbor/util/optional.hpp>
 #include <arbor/cable_cell.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/locset.hpp>
diff --git a/test/unit/test_cv_layout.cpp b/test/unit/test_cv_layout.cpp
index 8502318d9e66b6b03a3a8f7255baee08b4c21625..bcc0c5a32cf35a46281f2ccfebb84e9fe0617791 100644
--- a/test/unit/test_cv_layout.cpp
+++ b/test/unit/test_cv_layout.cpp
@@ -5,7 +5,6 @@
 #include <arbor/math.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/locset.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "fvm_layout.hpp"
 #include "util/span.hpp"
diff --git a/test/unit/test_cv_policy.cpp b/test/unit/test_cv_policy.cpp
index 2ffbfbc3afe4281020b6ccbbe2e12a9998d7031f..7a4dc6327e35b5bebec9b2d006e076a1864636fc 100644
--- a/test/unit/test_cv_policy.cpp
+++ b/test/unit/test_cv_policy.cpp
@@ -3,7 +3,6 @@
 #include <utility>
 #include <vector>
 
-#include <arbor/util/optional.hpp>
 #include <arbor/cable_cell.hpp>
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/morph/morphology.hpp>
diff --git a/test/unit/test_fvm_layout.cpp b/test/unit/test_fvm_layout.cpp
index 3613a251cc6cbb997cccd579baf00f40e6e16051..4c6dc4cbfe9b81896af722b4e9bb92e8d8dc170a 100644
--- a/test/unit/test_fvm_layout.cpp
+++ b/test/unit/test_fvm_layout.cpp
@@ -5,7 +5,6 @@
 #include <arbor/cable_cell.hpp>
 #include <arbor/math.hpp>
 #include <arbor/mechcat.hpp>
-#include <arbor/util/optional.hpp>
 
 #include "arbor/morph/morphology.hpp"
 #include "arbor/morph/segment_tree.hpp"
@@ -25,6 +24,7 @@ using namespace arb;
 
 using util::make_span;
 using util::count_along;
+using util::ptr_by_key;
 using util::value_by_key;
 
 namespace {
@@ -190,12 +190,12 @@ struct exp_instance {
     bool is_in(const arb::fvm_mechanism_config& C) const {
         std::vector<unsigned> _;
         auto part = util::make_partition(_, C.multiplicity);
-        auto& evals = *value_by_key(C.param_values, "e");
+        auto& evals = *ptr_by_key(C.param_values, "e");
         // Handle both expsyn and exp2syn by looking for "tau1" if "tau"
         // parameter is not found.
-        auto& tauvals = value_by_key(C.param_values, "tau")?
-            *value_by_key(C.param_values, "tau"):
-            *value_by_key(C.param_values, "tau1");
+        auto& tauvals = *(value_by_key(C.param_values, "tau")?
+            ptr_by_key(C.param_values, "tau"):
+            ptr_by_key(C.param_values, "tau1"));
 
         for (auto i: make_span(C.multiplicity.size())) {
             exp_instance other(C.cv[i],
@@ -445,11 +445,11 @@ TEST(fvm_layout, synapse_targets) {
 
     auto& expsyn_cv = M.mechanisms.at("expsyn").cv;
     auto& expsyn_target = M.mechanisms.at("expsyn").target;
-    auto& expsyn_e = value_by_key(M.mechanisms.at("expsyn").param_values, "e"s).value();
+    auto& expsyn_e = *ptr_by_key(M.mechanisms.at("expsyn").param_values, "e"s);
 
     auto& exp2syn_cv = M.mechanisms.at("exp2syn").cv;
     auto& exp2syn_target = M.mechanisms.at("exp2syn").target;
-    auto& exp2syn_e = value_by_key(M.mechanisms.at("exp2syn").param_values, "e"s).value();
+    auto& exp2syn_e = *ptr_by_key(M.mechanisms.at("exp2syn").param_values, "e"s);
 
     EXPECT_TRUE(util::is_sorted(expsyn_cv));
     EXPECT_TRUE(util::is_sorted(exp2syn_cv));
@@ -597,8 +597,8 @@ TEST(fvm_layout, density_norm_area) {
     ASSERT_EQ(1u, M.mechanisms.count("hh"));
     auto& hh_params = M.mechanisms.at("hh").param_values;
 
-    auto& gkbar = value_by_key(hh_params, "gkbar"s).value();
-    auto& gl = value_by_key(hh_params, "gl"s).value();
+    auto& gkbar = *ptr_by_key(hh_params, "gkbar"s);
+    auto& gl = *ptr_by_key(hh_params, "gl"s);
 
     EXPECT_TRUE(testing::seq_almost_eq<double>(expected_gkbar, gkbar));
     EXPECT_TRUE(testing::seq_almost_eq<double>(expected_gl, gl));
@@ -676,9 +676,9 @@ TEST(fvm_layout, density_norm_area_partial) {
 
     auto& hh_params = M.mechanisms.at("hh").param_values;
 
-    auto& gkbar = value_by_key(hh_params, "gkbar"s).value();
-    auto& gnabar = value_by_key(hh_params, "gnabar"s).value();
-    auto& gl = value_by_key(hh_params, "gl"s).value();
+    auto& gkbar = *ptr_by_key(hh_params, "gkbar"s);
+    auto& gnabar = *ptr_by_key(hh_params, "gnabar"s);
+    auto& gl = *ptr_by_key(hh_params, "gl"s);
 
     ASSERT_EQ(1u, gkbar.size());
     ASSERT_EQ(1u, gnabar.size());
diff --git a/test/unit/test_maputil.cpp b/test/unit/test_maputil.cpp
index 4cb6ed06d547de60005bbf56d41715ea2d748330..ea97cd5348426744ccd785ef61dbd298c0e41215 100644
--- a/test/unit/test_maputil.cpp
+++ b/test/unit/test_maputil.cpp
@@ -97,16 +97,11 @@ namespace {
             return std::map<K, V>::find(key);
         }
     };
-
-    template <typename X>
-    constexpr bool is_optional_reference(X) { return false; }
-
-    template <typename X>
-    constexpr bool is_optional_reference(util::optional<X&>) { return true; }
 }
 
 TEST(maputil, value_by_key_map) {
     using util::value_by_key;
+    using util::ptr_by_key;
 
     check_map<std::string, int> map_s2i = {
         {"fish", 4},
@@ -117,25 +112,22 @@ TEST(maputil, value_by_key_map) {
     EXPECT_FALSE(value_by_key(map_s2i, "deer"));
     EXPECT_EQ(1, S.count);
 
-    // Should get an optional reference if argument is an lvalue.
-
     S.count = 0;
     auto r1 = value_by_key(map_s2i, "sheep");
     EXPECT_EQ(1, S.count);
     EXPECT_TRUE(r1);
     EXPECT_EQ(5, r1.value());
-    EXPECT_TRUE(is_optional_reference(r1));
-    r1.value() = 6;
-    EXPECT_EQ(6, value_by_key(map_s2i, "sheep").value());
 
-    // Should not get an optional reference if argument is an rvalue.
+    auto p1 = ptr_by_key(map_s2i, "sheep");
+    ASSERT_TRUE(p1);
+    *p1 = 6;
+    EXPECT_EQ(6, value_by_key(map_s2i, "sheep").value());
 
     S.count = 0;
     auto r2 = value_by_key(check_map<std::string, int>(map_s2i), "fish");
     EXPECT_EQ(1, S.count);
     EXPECT_TRUE(r2);
     EXPECT_EQ(4, r2.value());
-    EXPECT_FALSE(is_optional_reference(r2));
 
     // Providing an explicit comparator should fall-back to serial search.
 
@@ -149,6 +141,7 @@ TEST(maputil, value_by_key_map) {
 
 TEST(maputil, value_by_key_sequence) {
     using util::value_by_key;
+    using util::ptr_by_key;
 
     // Note: value_by_key returns the value of `get<1>` on the
     // entries in the map or sequence.
@@ -165,14 +158,15 @@ TEST(maputil, value_by_key_sequence) {
     auto r1 = value_by_key(table, 3);
     EXPECT_TRUE(r1);
     EXPECT_EQ("three"s, r1.value());
-    EXPECT_TRUE(is_optional_reference(r1));
-    r1.value() = "four";
+
+    auto p1 = ptr_by_key(table, 3);
+    ASSERT_TRUE(p1);
+    *p1 = "four";
     EXPECT_EQ("four"s, value_by_key(table, 3).value());
 
     auto r2 = value_by_key(std::move(table), 1);
     EXPECT_TRUE(r2);
     EXPECT_EQ("one", r2.value());
-    EXPECT_FALSE(is_optional_reference(r2));
 }
 
 TEST(maputil, binary_search_index) {
diff --git a/test/unit/test_optional.cpp b/test/unit/test_optional.cpp
deleted file mode 100644
index 760e4a683a1d2ab7d8b64823c2c3323360b266f3..0000000000000000000000000000000000000000
--- a/test/unit/test_optional.cpp
+++ /dev/null
@@ -1,304 +0,0 @@
-#include <algorithm>
-#include <array>
-#include <string>
-#include <typeinfo>
-
-#include "../gtest.h"
-
-#include <arbor/util/optional.hpp>
-
-#include "common.hpp"
-
-using namespace std::string_literals;
-using namespace arb::util;
-
-TEST(optional, ctors) {
-    optional<int> a, b(3), c = b, d = 4;
-
-    ASSERT_FALSE((bool)a);
-    ASSERT_TRUE((bool)b);
-    ASSERT_TRUE((bool)c);
-    ASSERT_TRUE((bool)d);
-
-    EXPECT_EQ(3, b.value());
-    EXPECT_EQ(3, c.value());
-    EXPECT_EQ(4, d.value());
-}
-
-TEST(optional, unset_throw) {
-    optional<int> a;
-    int check = 10;
-
-    try {
-        a.value();
-    }
-    catch (optional_unset_error& e) {
-        ++check;
-    }
-    EXPECT_EQ(11, check);
-
-    check = 20;
-    a = 2;
-    try {
-        a.value();
-    }
-    catch (optional_unset_error& e) {
-        ++check;
-    }
-    EXPECT_EQ(20, check);
-
-    check = 30;
-    a.reset();
-    try {
-        a.value();
-    }
-    catch (optional_unset_error& e) {
-        ++check;
-    }
-    EXPECT_EQ(31, check);
-}
-
-TEST(optional, deref) {
-    struct foo {
-        int a;
-        explicit foo(int a_): a(a_) {}
-        double value() { return 3.0*a; }
-    };
-
-    optional<foo> f = foo(2);
-    EXPECT_EQ(6.0, f->value());
-    EXPECT_EQ(2, (*f).a);
-}
-
-TEST(optional, ctor_conv) {
-    optional<std::array<int, 3>> x{{1, 2, 3}};
-    EXPECT_EQ(3u, x->size());
-}
-
-TEST(optional, ctor_ref) {
-    int v = 10;
-    optional<int&> a(v);
-
-    EXPECT_EQ(10, a.value());
-    v = 20;
-    EXPECT_EQ(20, a.value());
-
-    optional<int&> b(a), c = b, d = v;
-    EXPECT_EQ(&(a.value()), &(b.value()));
-    EXPECT_EQ(&(a.value()), &(c.value()));
-    EXPECT_EQ(&(a.value()), &(d.value()));
-}
-
-TEST(optional, assign_returns) {
-    optional<int> a = 3;
-
-    auto b = (a = 4);
-    EXPECT_EQ(typeid(optional<int>), typeid(b));
-
-    auto bp = &(a = 4);
-    EXPECT_EQ(&a, bp);
-
-    auto b2 = (a = optional<int>(10));
-    EXPECT_EQ(typeid(optional<int>), typeid(b2));
-
-    auto bp2 = &(a = 4);
-    EXPECT_EQ(&a, bp2);
-
-    auto b3 = (a = nullopt);
-    EXPECT_EQ(typeid(optional<int>), typeid(b3));
-
-    auto bp3 = &(a = 4);
-    EXPECT_EQ(&a, bp3);
-}
-
-TEST(optional, assign_reference) {
-    double a = 3.0;
-    optional<double&> ar;
-    optional<double&> br;
-
-    ar = a;
-    EXPECT_TRUE(ar);
-    *ar = 5.0;
-    EXPECT_EQ(5.0, a);
-
-    auto& check_rval = (br = ar);
-    EXPECT_TRUE(br);
-    EXPECT_EQ(&br, &check_rval);
-
-    *br = 7.0;
-    EXPECT_EQ(7.0, a);
-
-    auto& check_rval2 = (br = nullopt);
-    EXPECT_FALSE(br);
-    EXPECT_EQ(&br, &check_rval2);
-}
-
-TEST(optional, ctor_nomove) {
-    using nomove = testing::nomove<int>;
-
-    optional<nomove> a(nomove(3));
-    EXPECT_EQ(nomove(3), a.value());
-
-    optional<nomove> b;
-    b = a;
-    EXPECT_EQ(nomove(3), b.value());
-
-    b = optional<nomove>(nomove(4));
-    EXPECT_EQ(nomove(4), b.value());
-}
-
-TEST(optional, ctor_nocopy) {
-    using nocopy = testing::nocopy<int>;
-
-    optional<nocopy> a(nocopy(5));
-    EXPECT_EQ(nocopy(5), a.value());
-
-    nocopy::reset_counts();
-    optional<nocopy> b(std::move(a));
-    EXPECT_EQ(nocopy(5), b.value());
-    EXPECT_EQ(0, a.value().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.value());
-    EXPECT_EQ(1, nocopy::move_ctor_count);
-    EXPECT_EQ(1, nocopy::move_assign_count);
-
-    nocopy::reset_counts();
-    nocopy v = optional<nocopy>(nocopy(9)).value();
-    EXPECT_EQ(2, nocopy::move_ctor_count);
-    EXPECT_EQ(nocopy(9), v.value);
-
-    const optional<nocopy> ccheck(nocopy(1));
-    EXPECT_TRUE(std::is_rvalue_reference<decltype(std::move(ccheck).value())>::value);
-    EXPECT_TRUE(std::is_const<std::remove_reference_t<decltype(std::move(ccheck).value())>>::value);
-}
-
-TEST(optional, value_or) {
-    optional<double> x = 3;
-    EXPECT_EQ(3., x.value_or(5));
-
-    x = nullopt;
-    EXPECT_EQ(5., x.value_or(5));
-
-    // `value_or` returns T for optional<T>:
-    struct check_conv {
-        bool value = false;
-        explicit check_conv(bool value): value(value) {}
-
-        explicit operator std::string() const {
-            return value? "true": "false";
-        }
-    };
-    check_conv cc{true};
-
-    optional<std::string> present = "present"s;
-    optional<std::string> absent; // nullopt
-
-    auto result = present.value_or(cc);
-    EXPECT_EQ(typeid(std::string), typeid(result));
-    EXPECT_EQ("present"s, result);
-
-    result = absent.value_or(cc);
-    EXPECT_EQ("true"s, result);
-
-    // Check move semantics in argument:
-
-    using nocopy = testing::nocopy<int>;
-
-    nocopy::reset_counts();
-    nocopy z1 = optional<nocopy>().value_or(nocopy(7));
-
-    EXPECT_EQ(7, z1.value);
-    EXPECT_EQ(1, nocopy::move_ctor_count);
-
-    nocopy::reset_counts();
-    nocopy z2 = optional<nocopy>(nocopy(3)).value_or(nocopy(7));
-
-    EXPECT_EQ(3, z2.value);
-    EXPECT_EQ(2, nocopy::move_ctor_count);
-}
-
-TEST(optional, ref_value_or) {
-    double a = 2.0;
-    double b = 3.0;
-
-    optional<double&> x = a;
-    double& ref1 = x.value_or(b);
-
-    EXPECT_EQ(2., ref1);
-
-    x = nullopt;
-    double& ref2 = x.value_or(b);
-
-    EXPECT_EQ(3., ref2);
-
-    ref1 = 12.;
-    ref2 = 13.;
-    EXPECT_EQ(12., a);
-    EXPECT_EQ(13., b);
-
-    const optional<double&> cx = x;
-    auto& ref3 = cx.value_or(b);
-    EXPECT_TRUE(std::is_const<std::remove_reference_t<decltype(ref3)>>::value);
-    EXPECT_EQ(&b, &ref3);
-}
-
-TEST(optional, void) {
-    optional<void> a, b(true), c(a), d = b, e(false), f(nullopt);
-
-    EXPECT_FALSE((bool)a);
-    EXPECT_TRUE((bool)b);
-    EXPECT_FALSE((bool)c);
-    EXPECT_TRUE((bool)d);
-    EXPECT_TRUE((bool)e);
-    EXPECT_FALSE((bool)f);
-
-    auto& check_rval = (b = nullopt);
-    EXPECT_FALSE((bool)b);
-    EXPECT_EQ(&b, &check_rval);
-}
-
-TEST(optional, conversion) {
-    optional<double> a(3), b = 5;
-    EXPECT_TRUE((bool)a);
-    EXPECT_TRUE((bool)b);
-    EXPECT_EQ(3.0, a.value());
-    EXPECT_EQ(5.0, b.value());
-
-    optional<int> x;
-    optional<double> c(x);
-    optional<double> d = optional<int>();
-    EXPECT_FALSE((bool)c);
-    EXPECT_FALSE((bool)d);
-}
-
-TEST(optional, just) {
-    int x = 3;
-
-    optional<int&> o1 = just(x);
-    optional<int>  o2 = just(x);
-
-    o1.value() = 4;
-    optional<int>  o3 = just(x);
-    EXPECT_EQ(4, o1.value());
-    EXPECT_EQ(3, o2.value());
-    EXPECT_EQ(4, o3.value());
-}
-
-TEST(optional, emplace) {
-    optional<int> o1(7);
-    optional<std::array<double, 3>> o2{{22., 22., 22.}};
-    int x = 42;
-    std::array<double, 3> arr{{4.5, 7.1, 1.2}};
-
-    o1.emplace(x);
-    o2.emplace(arr);
-
-    EXPECT_EQ(42, o1.value());
-    EXPECT_EQ(4.5, o2.value()[0]);
-    EXPECT_EQ(7.1, o2.value()[1]);
-    EXPECT_EQ(1.2, o2.value()[2]);
-}
diff --git a/test/unit/test_synapses.cpp b/test/unit/test_synapses.cpp
index de1a796b88763ffe33e31b3dc96c84469d43516e..6c9bdeb2b0e820870b317a27671da497d2a47e40 100644
--- a/test/unit/test_synapses.cpp
+++ b/test/unit/test_synapses.cpp
@@ -6,7 +6,6 @@
 
 #include <arbor/constants.hpp>
 #include <arbor/mechcat.hpp>
-#include <arbor/util/optional.hpp>
 #include <arbor/cable_cell.hpp>
 
 #include "backends/multicore/fvm.hpp"