diff --git a/arbor/fvm_lowered_cell.hpp b/arbor/fvm_lowered_cell.hpp
index a30e19d2fb08b6b313ca53147161c605d8b3b9c3..cd480f5917e859ec661fcf5fd298e55c74524aac 100644
--- a/arbor/fvm_lowered_cell.hpp
+++ b/arbor/fvm_lowered_cell.hpp
@@ -4,6 +4,7 @@
 #include <memory>
 #include <type_traits>
 #include <unordered_map>
+#include <variant>
 #include <vector>
 
 #include <arbor/assert.hpp>
@@ -13,7 +14,6 @@
 #include <arbor/morph/primitives.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/util/any_ptr.hpp>
-#include <arbor/util/variant.hpp>
 
 #include "backends/event.hpp"
 #include "backends/threshold_crossing.hpp"
@@ -41,10 +41,10 @@ struct fvm_integration_result {
 
 struct fvm_probe_scalar {
     probe_handle raw_handles[1] = {nullptr};
-    util::variant<mlocation, cable_probe_point_info> metadata;
+    std::variant<mlocation, cable_probe_point_info> metadata;
 
     util::any_ptr get_metadata_ptr() const {
-        return util::visit([](const auto& x) -> util::any_ptr { return &x; }, metadata);
+        return std::visit([](const auto& x) -> util::any_ptr { return &x; }, metadata);
     }
 };
 
@@ -58,15 +58,15 @@ struct fvm_probe_interpolated {
 
 struct fvm_probe_multi {
     std::vector<probe_handle> raw_handles;
-    util::variant<mcable_list, std::vector<cable_probe_point_info>> metadata;
+    std::variant<mcable_list, std::vector<cable_probe_point_info>> metadata;
 
     void shrink_to_fit() {
         raw_handles.shrink_to_fit();
-        util::visit([](auto& v) { v.shrink_to_fit(); }, metadata);
+        std::visit([](auto& v) { v.shrink_to_fit(); }, metadata);
     }
 
     util::any_ptr get_metadata_ptr() const {
-        return util::visit([](const auto& x) -> util::any_ptr { return &x; }, metadata);
+        return std::visit([](const auto& x) -> util::any_ptr { return &x; }, metadata);
     }
 };
 
@@ -122,7 +122,7 @@ struct fvm_probe_data {
     fvm_probe_data(fvm_probe_weighted_multi p): info(std::move(p)) {}
     fvm_probe_data(fvm_probe_membrane_currents p): info(std::move(p)) {}
 
-    util::variant<
+    std::variant<
         missing_probe_info,
         fvm_probe_scalar,
         fvm_probe_interpolated,
@@ -133,7 +133,7 @@ struct fvm_probe_data {
 
     auto raw_handle_range() const {
         return util::make_range(
-            util::visit(
+            std::visit(
                 [](auto& i) -> std::pair<const probe_handle*, const probe_handle*> {
                     using std::data;
                     using std::size;
@@ -143,12 +143,12 @@ struct fvm_probe_data {
     }
 
     util::any_ptr get_metadata_ptr() const {
-        return util::visit([](const auto& i) -> util::any_ptr { return i.get_metadata_ptr(); }, info);
+        return std::visit([](const auto& i) -> util::any_ptr { return i.get_metadata_ptr(); }, info);
     }
 
     sample_size_type n_raw() const { return raw_handle_range().size(); }
 
-    explicit operator bool() const { return !util::get_if<missing_probe_info>(info); }
+    explicit operator bool() const { return !std::get_if<missing_probe_info>(&info); }
 };
 
 // Samplers are tied to probe ids, but one probe id may
diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp
index ebb430a5b79e3b0bababa031ba1e198c9d727f4d..d36c3bd78f3fb6f16c2824d0344fb0f9ef45236e 100644
--- a/arbor/fvm_lowered_cell_impl.hpp
+++ b/arbor/fvm_lowered_cell_impl.hpp
@@ -20,7 +20,6 @@
 #include <arbor/common_types.hpp>
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/recipe.hpp>
-#include <arbor/util/any.hpp>
 #include <arbor/util/any_visitor.hpp>
 #include <arbor/util/optional.hpp>
 
@@ -145,7 +144,7 @@ private:
         std::vector<fvm_probe_data>& probe_data, // out parameter
         const std::vector<cable_cell>& cells,
         std::size_t cell_idx,
-        const util::any& paddr,
+        const std::any& paddr,
         const fvm_cv_discretization& D,
         const fvm_mechanism_data& M,
         const std::vector<target_handle>& handles,
@@ -356,7 +355,7 @@ void fvm_lowered_cell_impl<Backend>::initialize(
     std::vector<target_handle>& target_handles,
     probe_association_map& probe_map)
 {
-    using util::any_cast;
+    using std::any_cast;
     using util::count_along;
     using util::make_span;
     using util::value_by_key;
@@ -374,19 +373,19 @@ void fvm_lowered_cell_impl<Backend>::initialize(
                try {
                    cells[i] = any_cast<cable_cell&&>(rec.get_cell_description(gid));
                }
-               catch (util::bad_any_cast&) {
+               catch (std::bad_any_cast&) {
                    throw bad_cell_description(rec.get_cell_kind(gid), gid);
                }
            });
 
     cable_cell_global_properties global_props;
     try {
-        util::any rec_props = rec.get_global_properties(cell_kind::cable);
+        std::any rec_props = rec.get_global_properties(cell_kind::cable);
         if (rec_props.has_value()) {
             global_props = any_cast<cable_cell_global_properties>(rec_props);
         }
     }
-    catch (util::bad_any_cast&) {
+    catch (std::bad_any_cast&) {
         throw bad_global_property(cell_kind::cable);
     }
 
@@ -712,7 +711,7 @@ void fvm_lowered_cell_impl<Backend>::resolve_probe_address(
     std::vector<fvm_probe_data>& probe_data,
     const std::vector<cable_cell>& cells,
     std::size_t cell_idx,
-    const util::any& paddr,
+    const std::any& paddr,
     const fvm_cv_discretization& D,
     const fvm_mechanism_data& M,
     const std::vector<target_handle>& handles,
diff --git a/arbor/include/arbor/recipe.hpp b/arbor/include/arbor/recipe.hpp
index 2d8593f0da85a4d83b398b6434050557fba7c696..3e8d834f9a2f27d82440ce30f580036b2951fcfe 100644
--- a/arbor/include/arbor/recipe.hpp
+++ b/arbor/include/arbor/recipe.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <any>
 #include <utility>
 #include <vector>
 
@@ -13,7 +14,7 @@ struct probe_info {
     probe_tag tag;
 
     // Address type will be specific to cell kind of cell `id.gid`.
-    util::any address;
+    std::any address;
 
     probe_info(probe_info&) = default;
     probe_info(const probe_info&) = default;
@@ -89,7 +90,7 @@ public:
     }
 
     // Global property type will be specific to given cell kind.
-    virtual util::any get_global_properties(cell_kind) const { return util::any{}; };
+    virtual std::any get_global_properties(cell_kind) const { return std::any{}; };
 
     virtual ~recipe() {}
 };
diff --git a/arbor/include/arbor/symmetric_recipe.hpp b/arbor/include/arbor/symmetric_recipe.hpp
index 917ec892c022d3857e8d05d8dc20bcdc66e54abc..7addcf6fed71434325d763a61c41857a0101428b 100644
--- a/arbor/include/arbor/symmetric_recipe.hpp
+++ b/arbor/include/arbor/symmetric_recipe.hpp
@@ -1,15 +1,17 @@
 #pragma once
 
+#include <any>
 #include <cstddef>
 #include <memory>
 #include <unordered_map>
 #include <stdexcept>
 
 #include <arbor/recipe.hpp>
+#include <arbor/util/unique_any.hpp>
 
 namespace arb {
 
-// tile inherits from recipe but is not a regular recipe.
+// `tile` inherits from recipe but is not a regular recipe.
 // It is used to describe a recipe on a single rank
 // that will be translated to all other ranks using symmetric_recipe.
 // This means it can allow connections from gid >= ncells
@@ -19,7 +21,7 @@ public:
     virtual cell_size_type num_tiles() const { return 1; }
 };
 
-// symmetric recipe takes a tile and duplicates it across
+// `symmetric_recipe` takes a tile and duplicates it across
 // as many ranks as tile indicates. Its functions call the
 // underlying functions of tile and perform transformations
 // on the results when needed.
@@ -74,7 +76,7 @@ public:
         return tiled_recipe_->get_probes(i);
     }
 
-    util::any get_global_properties(cell_kind ck) const override {
+    std::any get_global_properties(cell_kind ck) const override {
         return tiled_recipe_->get_global_properties(ck);
     };
 
diff --git a/arbor/include/arbor/util/any.hpp b/arbor/include/arbor/util/any.hpp
deleted file mode 100644
index 7198f072d59d7408a50e8fc6bb45bbf6e5d96a66..0000000000000000000000000000000000000000
--- a/arbor/include/arbor/util/any.hpp
+++ /dev/null
@@ -1,215 +0,0 @@
-#pragma once
-
-#include <memory>
-#include <typeinfo>
-#include <type_traits>
-
-// Partial implementation of std::any from C++17 standard.
-//      http://en.cppreference.com/w/cpp/utility/any
-//
-// Implements a standard-compliant subset of the full interface.
-//
-// - Does not avoid dynamic allocation of small objects.
-// - Does not implement the in_place_type<T> constructors from the standard.
-// - Does not implement the emplace modifier from the standard.
-
-namespace arb {
-namespace util {
-
-// Defines a type of object to be thrown by the value-returning forms of
-// util::any_cast on failure.
-//      http://en.cppreference.com/w/cpp/utility/any/bad_any_cast
-class bad_any_cast: public std::bad_cast {
-public:
-    const char* what() const noexcept override {
-        return "bad any cast";
-    }
-};
-
-class any {
-public:
-    constexpr any() = default;
-
-    any(const any& other): state_(other.state_->copy()) {}
-
-    any(any&& other) noexcept {
-        std::swap(other.state_, state_);
-    }
-
-    template <
-        typename T,
-        typename = std::enable_if_t<!std::is_same<std::decay_t<T>, any>::value>
-    >
-    any(T&& other) {
-        using contained_type = std::decay_t<T>;
-        static_assert(std::is_copy_constructible<contained_type>::value,
-            "Type of contained object stored in any must satisfy the CopyConstructible requirements.");
-
-        state_.reset(new model<contained_type>(std::forward<T>(other)));
-    }
-
-    any& operator=(const any& other) {
-        state_.reset(other.state_->copy());
-        return *this;
-    }
-
-    any& operator=(any&& other) noexcept {
-        swap(other);
-        return *this;
-    }
-
-    template <
-        typename T,
-        typename = std::enable_if_t<!std::is_same<std::decay_t<T>, any>::value>
-    >
-    any& operator=(T&& other) {
-        using contained_type = std::decay_t<T>;
-
-        static_assert(std::is_copy_constructible<contained_type>::value,
-            "Type of contained object stored in any must satisfy the CopyConstructible requirements.");
-
-        state_.reset(new model<contained_type>(std::forward<T>(other)));
-        return *this;
-    }
-
-    void reset() noexcept {
-        state_.reset(nullptr);
-    }
-
-    void swap(any& other) noexcept {
-        std::swap(other.state_, state_);
-    }
-
-    bool has_value() const noexcept {
-        return (bool)state_;
-    }
-
-    const std::type_info& type() const noexcept {
-        return has_value()? state_->type(): typeid(void);
-    }
-
-private:
-    struct interface {
-        virtual ~interface() = default;
-        virtual const std::type_info& type() = 0;
-        virtual interface* copy() = 0;
-        virtual void* pointer() = 0;
-        virtual const void* pointer() const = 0;
-    };
-
-    template <typename T>
-    struct model: public interface {
-        ~model() = default;
-        model(const T& other): value(other) {}
-        model(T&& other): value(std::move(other)) {}
-
-        interface* copy() override { return new model<T>(*this); }
-        const std::type_info& type() override { return typeid(T); }
-        void* pointer() override { return &value; }
-        const void* pointer() const override { return &value; }
-
-        T value;
-    };
-
-    std::unique_ptr<interface> state_;
-
-protected:
-    template <typename T>
-    friend const T* any_cast(const any* operand);
-
-    template <typename T>
-    friend T* any_cast(any* operand);
-
-    template <typename T>
-    T* unsafe_cast() {
-        return static_cast<T*>(state_->pointer());
-    }
-
-    template <typename T>
-    const T* unsafe_cast() const {
-        return static_cast<const T*>(state_->pointer());
-    }
-};
-
-namespace impl {
-
-template <typename T>
-using any_cast_remove_qual = std::remove_cv_t<std::remove_reference_t<T>>;
-
-} // namespace impl
-
-// If operand is not a null pointer, and the typeid of the requested T matches
-// that of the contents of operand, a pointer to the value contained by operand,
-// otherwise a null pointer.
-template<class T>
-const T* any_cast(const any* operand) {
-    if (operand && operand->type()==typeid(T)) {
-        return operand->unsafe_cast<T>();
-    }
-    return nullptr;
-}
-
-template<class T>
-T* any_cast(any* operand) {
-    if (operand && operand->type()==typeid(T)) {
-        return operand->unsafe_cast<T>();
-    }
-    return nullptr;
-}
-
-template<class T>
-T any_cast(const any& operand) {
-    using U = impl::any_cast_remove_qual<T>;
-    static_assert(std::is_constructible<T, const U&>::value,
-        "any_cast type can't construct copy of contained object");
-
-    auto ptr = any_cast<U>(&operand);
-    if (ptr==nullptr) {
-        throw bad_any_cast();
-    }
-    return static_cast<T>(*ptr);
-}
-
-template<class T>
-T any_cast(any& operand) {
-    using U = impl::any_cast_remove_qual<T>;
-    static_assert(std::is_constructible<T, U&>::value,
-        "any_cast type can't construct copy of contained object");
-
-    auto ptr = any_cast<U>(&operand);
-    if (ptr==nullptr) {
-        throw bad_any_cast();
-    }
-    return static_cast<T>(*ptr);
-}
-
-template<class T>
-T any_cast(any&& operand) {
-    using U = impl::any_cast_remove_qual<T>;
-    static_assert(std::is_constructible<T, U>::value,
-        "any_cast type can't construct copy of contained object");
-
-    auto ptr = any_cast<U>(&operand);
-    if (ptr==nullptr) {
-        throw bad_any_cast();
-    }
-    return static_cast<T>(std::move(*ptr));
-}
-
-// Constructs an any object containing an object of type T, passing the
-// provided arguments to T's constructor.
-//
-// This does not exactly follow the standard, which states that
-// make_any is equivalent to
-//   return std::any(std::in_place_type<T>, std::forward<Args>(args)...);
-// i.e. that the contained object should be constructed in place, whereas
-// this implementation constructs the object, then moves it into the
-// contained object.
-// FIXME: rewrite with in_place_type when available.
-template <class T, class... Args>
-any make_any(Args&&... args) {
-    return any(T(std::forward<Args>(args) ...));
-}
-
-} // namespace util
-} // namespace arb
diff --git a/arbor/include/arbor/util/any_cast.hpp b/arbor/include/arbor/util/any_cast.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b9fce2b587924f0fd990f070f134d9a95d0b9d96
--- /dev/null
+++ b/arbor/include/arbor/util/any_cast.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+// arb::util::any_cast wraps std::any_cast for std::any objects.
+//
+// arb::util::any_cast also has specializations for arb::util::unique_any
+// and arb::util::any_pointer defined in the corresponding headers.
+
+#include <any>
+#include <type_traits>
+#include <utility>
+
+namespace arb {
+namespace util {
+
+template <
+    typename T,
+    typename Any,
+    typename = std::enable_if_t<std::is_same_v<std::any, std::remove_cv_t<std::remove_reference_t<Any>>>>
+>
+T any_cast(Any&& a) { return std::any_cast<T>(std::forward<Any>(a)); }
+
+template <typename T>
+const T* any_cast(const std::any* p) noexcept { return std::any_cast<T>(p); }
+
+template <typename T>
+T* any_cast(std::any* p) noexcept { return std::any_cast<T>(p); }
+
+} // namespace util
+} // namespace arb
diff --git a/arbor/include/arbor/util/any_ptr.hpp b/arbor/include/arbor/util/any_ptr.hpp
index 2587511bd7a3541e14cd2b7555e386f2eb32a4c9..f148c566bbd5455d32dd5158c3a6a7942060d09b 100644
--- a/arbor/include/arbor/util/any_ptr.hpp
+++ b/arbor/include/arbor/util/any_ptr.hpp
@@ -1,27 +1,35 @@
 #pragma once
 
-/* Specialied type erasure for pointer types.
- *
- * `any_ptr` represents a non-owning pointer to an arbitrary type
- * that can be confirmed at run-time.
- *
- * Semantics:
- *
- * 1. An `any_ptr` value p represents either a null pointer, or
- *    a non-null pointer of a specific but arbitrary type T.
- *
- * 2. The value of the pointer as a `void*` value can be retrieved
- *    with the member function `as<void*>()`.
- *
- * 3. The value of the pointer as a type T which is not `void*` is
- *    retrieved with the member function `as<T>()`. If the represented
- *    pointer is the null pointer or a pointer to a different type,
- *    `as<T>()` will return the null pointer.
- */
+// Specialied type erasure for pointer types.
+//
+// `any_ptr` represents a non-owning pointer to an arbitrary type
+// that can be confirmed at run-time.
+//
+// Semantics:
+//
+// 1. An `any_ptr` value p represents either a null pointer, or
+//    a non-null pointer of a specific but arbitrary type T.
+//
+// 2. The value of the pointer as a `void*` value can be retrieved
+//    with the member function `as<void*>()`.
+//
+// 3. The value of the pointer as a pointer of type T is retrieved
+//    with the member function `as<T>()`.
+//
+// 4. When U is not `void *` and is not the type T, the member function
+//    will return `nullptr`.
+//
+// Overloads are provided for `util::any_cast`, with
+// `util::any_cast<U>(anyp)` equal to `anyp.as<U>()` for a value
+// `anyp` of type `util::any_ptr.
+//
+// `any_ptr` values are ordered by the value of the corresponding
+// `void*` pointer.
 
 #include <cstddef>
 #include <type_traits>
 
+#include <arbor/util/any_cast.hpp>
 #include <arbor/util/lexcmp_def.hpp>
 
 namespace arb {
diff --git a/arbor/include/arbor/util/any_visitor.hpp b/arbor/include/arbor/util/any_visitor.hpp
index 48e5dfc02c762436362c7a23c0afc48ff07698b1..d579a5ad367390e90c37e972a5603df5520928f5 100644
--- a/arbor/include/arbor/util/any_visitor.hpp
+++ b/arbor/include/arbor/util/any_visitor.hpp
@@ -4,14 +4,13 @@
 // `operator()` from a number of functions or function objects.
 //
 // Provides the `any_visitor` class, which will call a provided functional
-// with the contained value in a `util::any` if it is one of a fixed set
+// with the contained value in a `std::any` if it is one of a fixed set
 // of types.
 
+#include <any>
 #include <utility>
 #include <type_traits>
 
-#include <arbor/util/any.hpp>
-
 namespace arb {
 namespace util {
 
@@ -35,7 +34,7 @@ using propagate_qualifier_t = typename propagate_qualifier<X, Y>::type;
 } // namespace impl
 
 // A type `any_visitor<A, B, ...>` has one public static method
-// `visit(f, a)` where `a` is a possibly const lvalue or rvalue util::any,
+// `visit(f, a)` where `a` is a possibly const lvalue or rvalue std::any,
 // and `f` is a functional object or function pointer.
 //
 // If `a` contains a value of any of the types `A, B, ...`, `f` will
@@ -55,9 +54,9 @@ struct any_visitor<T> {
         template <typename A>
         static auto visit(F&& f, A&& a) {
             using Q = impl::propagate_qualifier_t<A, T>;
-            return util::any_cast<T>(&a)?
-                   std::forward<F>(f)(util::any_cast<Q&&>(std::forward<A>(a))):
-                   throw ::arb::util::bad_any_cast();
+            return std::any_cast<T>(&a)?
+                   std::forward<F>(f)(std::any_cast<Q&&>(std::forward<A>(a))):
+                   throw std::bad_any_cast();
         }
     };
 
@@ -66,14 +65,14 @@ struct any_visitor<T> {
         template <typename A>
         static auto visit(F&& f, A&& a) {
             using Q = impl::propagate_qualifier_t<A, T>;
-            return util::any_cast<T>(&a)?
-                   std::forward<F>(f)(util::any_cast<Q&&>(std::forward<A>(a))):
+            return std::any_cast<T>(&a)?
+                   std::forward<F>(f)(std::any_cast<Q&&>(std::forward<A>(a))):
                    std::forward<F>(f)();
         }
     };
 
     template <typename F, typename A,
-        typename = std::enable_if_t<std::is_same<util::any, std::decay_t<A>>::value>
+        typename = std::enable_if_t<std::is_same_v<std::any, std::decay_t<A>>>
     >
     static auto visit(F&& f, A&& a) {
         return invoke_or_throw<F>::visit(std::forward<F>(f), std::forward<A>(a));
@@ -83,32 +82,18 @@ struct any_visitor<T> {
 template <typename T, typename U, typename... Rest>
 struct any_visitor<T, U, Rest...> {
     template <typename F, typename A,
-        typename = std::enable_if_t<std::is_same<util::any, std::decay_t<A>>::value>
+        typename = std::enable_if_t<std::is_same_v<std::any, std::decay_t<A>>>
     >
     static auto visit(F&& f, A&& a) {
         using Q = impl::propagate_qualifier_t<A, T>;
-        return util::any_cast<T>(&a)?
-               std::forward<F>(f)(util::any_cast<Q&&>(std::forward<A>(a))):
+        return std::any_cast<T>(&a)?
+               std::forward<F>(f)(std::any_cast<Q&&>(std::forward<A>(a))):
                any_visitor<U, Rest...>::visit(std::forward<F>(f), std::forward<A>(a));
     }
 };
 
 namespace impl {
 
-template <typename F, typename... A>
-struct invocable_impl {
-    template <typename G, typename = void>
-    struct test: std::false_type {};
-
-    template <typename G>
-    struct test<G, std::void_t<decltype(std::declval<G>()(std::declval<A>()...))>>: std::true_type {};
-
-    using type = typename test<F>::type;
-};
-
-template <typename F, typename... A>
-using invocable = typename invocable_impl<F, A...>::type;
-
 template <typename, typename...> struct overload_impl {};
 
 template <typename F1>
@@ -117,7 +102,7 @@ struct overload_impl<F1> {
 
     overload_impl(F1&& f1): f_(std::forward<F1>(f1)) {}
 
-    template <typename... A, std::enable_if_t<invocable<F1, A...>::value, int> = 0>
+    template <typename... A, std::enable_if_t<std::is_invocable_v<F1, A...>, int> = 0>
     decltype(auto) operator()(A&&... a) { return f_(std::forward<A>(a)...); }
 };
 
@@ -129,10 +114,10 @@ struct overload_impl<F1, F2, Fn...>: overload_impl<F2, Fn...> {
         overload_impl<F2, Fn...>(std::forward<F2>(f2), std::forward<Fn>(fn)...),
         f_(std::forward<F1>(f1)) {}
 
-    template <typename... A, std::enable_if_t<invocable<F1, A...>::value, int> = 0>
+    template <typename... A, std::enable_if_t<std::is_invocable_v<F1, A...>, int> = 0>
     decltype(auto) operator()(A&&... a) { return f_(std::forward<A>(a)...); }
 
-    template <typename... A, std::enable_if_t<!invocable<F1, A...>::value, int> = 0>
+    template <typename... A, std::enable_if_t<!std::is_invocable_v<F1, A...>, int> = 0>
     decltype(auto) operator()(A&&... a) {
         return overload_impl<F2, Fn...>::operator()(std::forward<A>(a)...);
     }
diff --git a/arbor/include/arbor/util/expected.hpp b/arbor/include/arbor/util/expected.hpp
index 17e37a80451947db697050b2d14ca500e413b57c..dd90d523dcc7c31c4f69e9919a9343116494b75e 100644
--- a/arbor/include/arbor/util/expected.hpp
+++ b/arbor/include/arbor/util/expected.hpp
@@ -186,6 +186,7 @@ struct expected {
     template <
         typename S,
         typename F,
+        typename = std::enable_if_t<!std::is_same_v<expected, expected<S, 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>>>
     >
@@ -196,6 +197,7 @@ struct expected {
     template <
         typename S,
         typename F,
+        typename = std::enable_if_t<!std::is_same_v<expected, expected<S, 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>>>
     >
@@ -205,10 +207,10 @@ struct expected {
 
     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>>>
+        typename = std::enable_if_t<!std::is_same_v<unexpected<E>, detail::remove_cvref_t<S>>>,
+        typename = std::enable_if_t<std::is_constructible_v<T, S&&>>
     >
     expected(S&& x): data_(std::in_place_index<0>, std::forward<S>(x)) {}
 
diff --git a/arbor/include/arbor/util/typed_map.hpp b/arbor/include/arbor/util/typed_map.hpp
index 22b59433106e0cbc8b9a6035dc2c29fde97cf393..1bd6214da96a72f6bedd082850e2822bb4ffd297 100644
--- a/arbor/include/arbor/util/typed_map.hpp
+++ b/arbor/include/arbor/util/typed_map.hpp
@@ -13,12 +13,11 @@
 //     m.get<int>() = {1, 2, 3};
 //     m.get<double>() = {1.2, 2.3};
 
+#include <any>
 #include <tuple>
 #include <typeindex>
 #include <unordered_map>
 
-#include <arbor/util/any.hpp>
-
 namespace arb {
 
 template <template <class> class E>
@@ -27,19 +26,19 @@ struct dynamic_typed_map {
     // default value if no entry in map for T.
     template <typename T>
     E<T>& get() {
-        arb::util::any& store_entry = tmap_[std::type_index(typeid(T))];
+        std::any& store_entry = tmap_[std::type_index(typeid(T))];
         if (!store_entry.has_value()) {
-            store_entry = arb::util::any(E<T>{});
+            store_entry = std::any(E<T>{});
         }
 
-        return arb::util::any_cast<E<T>&>(store_entry);
+        return std::any_cast<E<T>&>(store_entry);
     }
 
     // Retrieve value by const reference associated with type T;
     // throw if no entry in map for T.
     template <typename T>
     const E<T>& get() const {
-        return arb::util::any_cast<const E<T>&>(tmap_.at(std::type_index(typeid(T))));
+        return std::any_cast<const E<T>&>(tmap_.at(std::type_index(typeid(T))));
     }
 
     // True if map has an entry for type T.
@@ -47,7 +46,7 @@ struct dynamic_typed_map {
     bool has() const { return tmap_.count(std::type_index(typeid(T))); }
 
 private:
-    std::unordered_map<std::type_index, arb::util::any> tmap_;
+    std::unordered_map<std::type_index, std::any> tmap_;
 };
 
 template <template <class> class E, typename... Keys>
diff --git a/arbor/include/arbor/util/unique_any.hpp b/arbor/include/arbor/util/unique_any.hpp
index d5b72f6f63888574279d1c9e1e00d92da798585d..0e5d7834e03e8220e0ee3a1a8600349580da5839 100644
--- a/arbor/include/arbor/util/unique_any.hpp
+++ b/arbor/include/arbor/util/unique_any.hpp
@@ -1,51 +1,55 @@
 #pragma once
 
+#include <any>
 #include <memory>
 #include <typeinfo>
 #include <type_traits>
 
-#include <arbor/util/any.hpp>
+#include <arbor/util/any_cast.hpp>
 
-// A non copyable variant of util::any.
-// The two main use cases for such a container are
-//  1. storing types that are not copyable.
-//  2. ensuring that no copies are made of copyable types that have to be stored
-//     in a type-erased container.
+// A non copyable variant of std::any. The two main use cases for such a
+// container are:
+//     1. storing types that are not copyable, and
+//     2. ensuring that no copies are made of copyable types stored within a
+//        in a type-erased container.
 //
-// unique_any has the same semantics as any with the execption of copy and copy
-// assignment, which are explicitly forbidden for all contained types.
-// The requirement that the contained type be copy constructable has also been
-// relaxed.
+// util::unique_any has the same semantics as std::any with the execption of
+// copy and copy assignment, which are explicitly forbidden for all contained
+// types. Objects stored in util::unique_any also naturally need not be copy
+// constructable.
 //
-// The any_cast non-member functions have been overridden for unique_any, with
-// the same semantics as for any.
-// This makes it possible to copy the underlying stored type if the type is
-// copyable. For example, the following code will compile and execute as
-// expected.
+// util::any_cast<T> overloads are provided for util::unique_any, with
+// semantics analagous to those of std::any_cast and std::any, with copying
+// of the contained value permitted if the type of that value permits it.
 //
-//  unique_any<int> a(3);
-//  int& ref = any_cast<int&>(a); // take a reference
-//  ref = 42;                     // update contained value via reference
-//  int  val = any_cast<int>(a);  // take a copy
-//  assert(val==42);
+// Examples:
 //
-// If the underlying type is not copyable, only references may be taken
+//      unique_any<int> a(3);
+//      int& ref = any_cast<int&>(a); // Take a reference.
+//      ref = 42;                     // Update contained value via reference.
+//      int val = any_cast<int>(a);   // Take a copy.
+//      assert(val==42);
 //
-//  unique_any<nocopy_t> a();
-//  nocopy_t& ref        = any_cast<nocopy_t&>(a);       // ok
-//  const nocopy_t& cref = any_cast<const nocopy_t&>(a); // ok
-//  nocopy_t v           = any_cast<nocopy_t>(a);        // compile time error
+//      // If the underlying type is not copyable, only references may be
+//      // taken to the contained value. For a movable but non-copyable
+//      // type `nocopy_t`:
 //
-// An lvalue can be created by moving from the contained object:
-//
-//  nocopy_t v = any_cast<nocopy_t&&>(std::move(a)); // ok
-//
-// After which a is in moved from state.
-
+//      unique_any<nocopy_t> a();
+//      nocopy_t& ref = any_cast<nocopy_t&>(a);              // ok
+//      const nocopy_t& cref = any_cast<const nocopy_t&>(a); // ok
+//      nocopy_t v = any_cast<nocopy_t&&>(std::move(a));     // ok
+//      nocopy_t v = any_cast<nocopy_t>(a);   // Not ok: compile-time error.
 
 namespace arb {
 namespace util {
 
+namespace detail {
+    // TODO: C++20 replace with std::remove_cvref_t
+    template <typename T>
+    using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
+}
+
+
 class unique_any {
 public:
     constexpr unique_any() = default;
@@ -56,7 +60,8 @@ public:
 
     template <
         typename T,
-        typename = std::enable_if_t<!std::is_same<std::decay_t<T>, unique_any>::value>
+        typename = std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<T>, unique_any>>,
+        typename = std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<T>, std::any>>
     >
     unique_any(T&& other) {
         state_.reset(new model<contained_type<T>>(std::forward<T>(other)));
@@ -69,7 +74,8 @@ public:
 
     template <
         typename T,
-        typename = std::enable_if_t<!std::is_same<std::decay_t<T>, unique_any>::value>
+        typename = std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<T>, unique_any>>,
+        typename = std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<T>, std::any>>
     >
     unique_any& operator=(T&& other) {
         state_.reset(new model<contained_type<T>>(std::forward<T>(other)));
@@ -160,40 +166,40 @@ T* any_cast(unique_any* operand) {
 
 template<class T>
 T any_cast(const unique_any& operand) {
-    using U = impl::any_cast_remove_qual<T>;
+    using U = detail::remove_cvref_t<T>;
     static_assert(std::is_constructible<T, const U&>::value,
         "any_cast type can't construct copy of contained object");
 
     auto ptr = any_cast<U>(&operand);
     if (ptr==nullptr) {
-        throw bad_any_cast();
+        throw std::bad_any_cast();
     }
     return static_cast<T>(*ptr);
 }
 
 template<class T>
 T any_cast(unique_any& operand) {
-    using U = impl::any_cast_remove_qual<T>;
+    using U = detail::remove_cvref_t<T>;
     static_assert(std::is_constructible<T, U&>::value,
         "any_cast type can't construct copy of contained object");
 
     auto ptr = any_cast<U>(&operand);
     if (ptr==nullptr) {
-        throw bad_any_cast();
+        throw std::bad_any_cast();
     }
     return static_cast<T>(*ptr);
 }
 
 template<class T>
 T any_cast(unique_any&& operand) {
-    using U = impl::any_cast_remove_qual<T>;
+    using U = detail::remove_cvref_t<T>;
 
     static_assert(std::is_constructible<T, U&&>::value,
         "any_cast type can't construct copy of contained object");
 
     auto ptr = any_cast<U>(&operand);
     if (ptr==nullptr) {
-        throw bad_any_cast();
+        throw std::bad_any_cast();
     }
     return static_cast<T>(std::move(*ptr));
 }
diff --git a/arbor/include/arbor/util/variant.hpp b/arbor/include/arbor/util/variant.hpp
deleted file mode 100644
index ecb724a1e1d4360a86193bf286d8479ceeaf1264..0000000000000000000000000000000000000000
--- a/arbor/include/arbor/util/variant.hpp
+++ /dev/null
@@ -1,638 +0,0 @@
-#pragma once
-
-// C++14 std::variant work-alike.
-//
-// Key differences:
-//
-//   * Using a type-index on operations where the type appears multiple times
-//     in the variant type list is not treated as an error.
-//
-//   * No template constants in C++14, so `in_place_index` and `in_place_type`
-//     are constexpr functions instead.
-//
-//   * Rather than overload `std::get` etc., uses `util::get` which wraps
-//     dispatches to `variant<...>::get` (`util::get` is also defined in
-//     private `util/meta.hpp` header for pairs and tuples.)
-//
-//   * Assignemnt from non-variant type relies upon default conversion to
-//     variant type.
-//
-//   * Swap doesn't make nothrow guarantees.
-//
-//   * Unimplemented (yet): visit() with more than one variant argument;
-//     monostate; comparisons; unit tests for nothrow guarantees.
-
-#include <cstddef>
-#include <new>
-#include <stdexcept>
-#include <type_traits>
-
-namespace arb {
-namespace util {
-
-struct bad_variant_access: public std::runtime_error {
-    bad_variant_access(): std::runtime_error("bad variant access") {}
-};
-
-template <typename T> struct in_place_type_t {};
-
-template <typename T>
-static constexpr in_place_type_t<T> in_place_type() { return {}; }
-
-template <std::size_t I> struct in_place_index_t {};
-
-template <std::size_t I>
-static constexpr in_place_index_t<I> in_place_index() { return {}; };
-
-namespace detail {
-
-template <typename... T>
-struct max_sizeof: public std::integral_constant<std::size_t, 1> {};
-
-template <typename H, typename... T>
-struct max_sizeof<H, T...>: public std::integral_constant<std::size_t,
-    (max_sizeof<T...>::value > sizeof(H))? max_sizeof<T...>::value: sizeof(H)> {};
-
-template <typename... T>
-struct max_alignof: public std::integral_constant<std::size_t, 1> {};
-
-template <typename H, typename... T>
-struct max_alignof<H, T...>: public std::integral_constant<std::size_t,
-    (max_alignof<T...>::value > alignof(H))? max_alignof<T...>::value: alignof(H)> {};
-
-// type_select_t<i, T0, ..., Tn> gives type Ti.
-
-template <std::size_t I, typename... T> struct type_select;
-
-template <typename X, typename... T>
-struct type_select<0, X, T...> { using type = X; };
-
-template <std::size_t I, typename X, typename... T>
-struct type_select<I, X, T...> { using type = typename type_select<I-1, T...>::type; };
-
-template <std::size_t I>
-struct type_select<I> { using type = void; };
-
-template <std::size_t I, typename... T>
-using type_select_t = typename type_select<I, T...>::type;
-
-// type_index<T, T0, ..., Tn>::value gives i such that T is Ti, or else -1.
-
-template <std::size_t I, typename X, typename... T>
-struct type_index_impl: std::integral_constant<std::size_t, std::size_t(-1)> {};
-
-template <std::size_t I, typename X, typename... T>
-struct type_index_impl<I, X, X, T...>: std::integral_constant<std::size_t, I> {};
-
-template <std::size_t I, typename X, typename Y, typename... T>
-struct type_index_impl<I, X, Y, T...>: type_index_impl<I+1, X, T...> {};
-
-template <typename X, typename... T>
-using type_index = std::integral_constant<std::size_t, type_index_impl<0, X, T...>::value>;
-
-// Build overload set for implicit construction from type list.
-
-template <typename T>
-using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
-
-template <std::size_t, typename... T>
-struct variant_implicit_ctor_index_impl;
-
-template <std::size_t I>
-struct variant_implicit_ctor_index_impl<I> {
-    static std::integral_constant<std::size_t, std::size_t(-1)> index(...);
-};
-
-template <std::size_t I, typename X, typename... T>
-struct variant_implicit_ctor_index_impl<I, X, T...>: variant_implicit_ctor_index_impl<I+1, T...> {
-    using variant_implicit_ctor_index_impl<I+1, T...>::index;
-
-    template <typename X_nocv = std::remove_cv_t<X>,
-              typename = std::enable_if_t<!std::is_same<bool, X_nocv>::value>>
-    static std::integral_constant<std::size_t, I> index(X);
-
-    template <typename A,
-              typename X_nocv = std::remove_cv_t<X>,
-              typename = std::enable_if_t<std::is_same<bool, X_nocv>::value>,
-              typename A_nocvref = remove_cvref_t<A>,
-              typename = std::enable_if_t<std::is_same<bool, A_nocvref>::value>>
-    static std::integral_constant<std::size_t, I> index(A&& a);
-};
-
-template <typename X, typename... T>
-struct variant_implicit_ctor_index:
-    decltype(variant_implicit_ctor_index_impl<0, T...>::index(std::declval<X>())) {};
-
-// Test for in-place types
-
-template <typename X> struct is_in_place_impl: std::false_type {};
-template <typename T> struct is_in_place_impl<in_place_type_t<T>>: std::true_type {};
-template <std::size_t I> struct is_in_place_impl<in_place_index_t<I>>: std::true_type {};
-
-template <typename X> using is_in_place = is_in_place_impl<std::decay_t<X>>;
-
-// Variadic tests for nothrow.
-
-template <typename... T> struct are_nothrow_move_constructible;
-template <> struct are_nothrow_move_constructible<>: std::true_type {};
-template <typename H, typename... T>
-struct are_nothrow_move_constructible<H, T...>:
-    std::conditional_t<std::is_nothrow_move_constructible<H>::value,
-                       are_nothrow_move_constructible<T...>, std::false_type> {};
-
-template <typename... T> struct are_nothrow_copy_constructible;
-template <> struct are_nothrow_copy_constructible<>: std::true_type {};
-template <typename H, typename... T>
-struct are_nothrow_copy_constructible<H, T...>:
-    std::conditional_t<std::is_nothrow_copy_constructible<H>::value,
-                       are_nothrow_copy_constructible<T...>, std::false_type> {};
-
-template <typename... T> struct any_reference;
-template <> struct any_reference<>: std::false_type {};
-template <typename H, typename... T>
-struct any_reference<H, T...>:
-    std::conditional_t<std::is_reference<H>::value, std::true_type, any_reference<T...>> {};
-
-// Copy and move ctor and assignment implementations.
-
-template <typename... T>
-struct variant_dynamic_impl;
-
-template <>
-struct variant_dynamic_impl<> {
-    static void copy(std::size_t i, char* data, const char* from) {
-        if (i!=std::size_t(-1)) throw bad_variant_access{};
-    }
-
-    static void move(std::size_t i, char* data, const char* from) {
-        if (i!=std::size_t(-1)) throw bad_variant_access{};
-    }
-
-    static void assign(std::size_t i, char* data, const char* from) {
-        if (i!=std::size_t(-1)) throw bad_variant_access{};
-    }
-
-    static void move_assign(std::size_t i, char* data, char* from) {
-        if (i!=std::size_t(-1)) throw bad_variant_access{};
-    }
-
-    static void swap(std::size_t i, char* data1, char* data2) {
-        if (i!=std::size_t(-1)) throw bad_variant_access{};
-    }
-
-    static void destroy(std::size_t i, char* data) {}
-
-    static bool cmp_eq(std::size_t i, const char* left, const char* right) {
-        return i==std::size_t(-1)? true: throw bad_variant_access{};
-    }
-
-    static bool cmp_ne(std::size_t i, const char* left, const char* right) {
-        return i==std::size_t(-1)? false: throw bad_variant_access{};
-    }
-};
-
-template <typename H, typename... T>
-struct variant_dynamic_impl<H, T...> {
-    static void copy(std::size_t i, char* data, const char* from) {
-        if (i==0) {
-            new(reinterpret_cast<H*>(data)) H(*reinterpret_cast<const H*>(from));
-        }
-        else {
-            variant_dynamic_impl<T...>::copy(i-1, data, from);
-        }
-    }
-
-    static void move(std::size_t i, char* data, char* from) {
-        if (i==0) {
-            new(reinterpret_cast<H*>(data)) H(std::move(*reinterpret_cast<H*>(from)));
-        }
-        else {
-            variant_dynamic_impl<T...>::move(i-1, data, from);
-        }
-    }
-
-    static void assign(std::size_t i, char* data, const char* from) {
-        if (i==0) {
-            *reinterpret_cast<H*>(data) = *reinterpret_cast<const H*>(from);
-        }
-        else {
-            variant_dynamic_impl<T...>::assign(i-1, data, from);
-        }
-    }
-
-    static void move_assign(std::size_t i, char* data, char* from) {
-        if (i==0) {
-            *reinterpret_cast<H*>(data) = std::move(*reinterpret_cast<H*>(from));
-        }
-        else {
-            variant_dynamic_impl<T...>::move_assign(i-1, data, from);
-        }
-    }
-
-    static void swap(std::size_t i, char* data1, char* data2) {
-        using std::swap;
-        if (i==0) {
-            swap(*reinterpret_cast<H*>(data1), *reinterpret_cast<H*>(data2));
-        }
-        else {
-            variant_dynamic_impl<T...>::swap(i-1, data1, data2);
-        }
-    }
-
-    static void destroy(std::size_t i, char* data) {
-        if (i==0) {
-            reinterpret_cast<H*>(data)->~H();
-        }
-        else {
-            variant_dynamic_impl<T...>::destroy(i-1, data);
-        }
-    }
-
-    static bool cmp_eq(std::size_t i, const char* left, const char* right) {
-        return i==0?
-               *reinterpret_cast<const H*>(left)==*reinterpret_cast<const H*>(right):
-               variant_dynamic_impl<T...>::cmp_eq(i-1, left, right);
-    }
-
-    static bool cmp_ne(std::size_t i, const char* left, const char* right) {
-        return i==0?
-               *reinterpret_cast<const H*>(left)!=*reinterpret_cast<const H*>(right):
-               variant_dynamic_impl<T...>::cmp_ne(i-1, left, right);
-    }
-};
-
-template <typename... T>
-struct variant {
-    static_assert(!any_reference<T...>::value, "variant must have no reference alternative");
-    alignas(max_alignof<T...>::value) char data[max_sizeof<T...>::value];
-
-    template <typename X> X* data_ptr() noexcept { return reinterpret_cast<X*>(&data); }
-    template <typename X> const X* data_ptr() const noexcept { return reinterpret_cast<const X*>(&data); }
-
-    std::size_t which_ = -1;
-    static constexpr std::size_t npos = -1;
-
-    // Explict construction by index.
-
-    template <std::size_t I, typename... A, typename = std::enable_if_t<(I<sizeof...(T))>>
-    explicit variant(in_place_index_t<I>, A&&... a): which_(I)
-    {
-        using X = type_select_t<I, T...>;
-        new(data_ptr<X>()) X(std::forward<A>(a)...);
-    }
-
-    template <std::size_t I, typename U, typename... A, typename = std::enable_if_t<(I<sizeof...(T))>>
-    explicit variant(in_place_index_t<I>, std::initializer_list<U> il, A&&... a): which_(I)
-    {
-        using X = type_select_t<I, T...>;
-        new(data_ptr<X>()) X(il, std::forward<A>(a)...);
-    }
-
-    // Explicit construction by type.
-
-    template <typename X, typename... A, std::size_t I = type_index<X, T...>::value>
-    explicit variant(in_place_type_t<X>, A&&... a):
-        variant(in_place_index_t<I>{}, std::forward<A>(a)...) {}
-
-    template <typename X, typename U, typename... A, std::size_t I = type_index<X, T...>::value>
-    explicit variant(in_place_type_t<X>, std::initializer_list<U> il, A&&... a):
-        variant(in_place_index_t<I>{}, il, std::forward<A>(a)...) {}
-
-    // Implicit construction from argument.
-
-    template <typename X,
-              typename = std::enable_if_t<!std::is_same<variant, std::decay_t<X>>::value>,
-              typename = std::enable_if_t<!is_in_place<X>::value>,
-              typename index = variant_implicit_ctor_index<X, T...>>
-    variant(X&& x):
-        variant(in_place_index<index::value>(), std::forward<X>(x)) {}
-
-    // Default constructible if first type is.
-
-    template <typename X = type_select_t<0, T...>,
-        typename = std::enable_if_t<std::is_default_constructible<X>::value>>
-    variant() noexcept(std::is_nothrow_default_constructible<X>::value): which_(0) {
-        new(data_ptr<X>()) X;
-    }
-
-    // Copy construction.
-
-    variant(const variant& x)
-        noexcept(are_nothrow_copy_constructible<T...>::value): which_(x.which_)
-    {
-        variant_dynamic_impl<T...>::copy(which_, data, x.data);
-    }
-
-    // Move construction.
-
-    variant(variant&& x)
-        noexcept(are_nothrow_move_constructible<T...>::value): which_(x.which_)
-    {
-        variant_dynamic_impl<T...>::move(which_, data, x.data);
-    }
-
-    // Copy assignment.
-
-    variant& operator=(const variant& x) {
-        if (which_!=x.which_) {
-            variant_dynamic_impl<T...>::destroy(which_, data);
-            which_ = npos;
-            if (x.which_!=npos) {
-                variant_dynamic_impl<T...>::copy(x.which_, data, x.data);
-                which_ = x.which_;
-            }
-        }
-        else {
-            auto old_which = which_;
-            which_ = npos;
-            if (x.which_!=npos) {
-                try {
-                    variant_dynamic_impl<T...>::assign(x.which_, data, x.data);
-                    which_ = x.which_;
-                }
-                catch (...) {
-                    variant_dynamic_impl<T...>::destroy(old_which, data);
-                    throw;
-                }
-            }
-        }
-        return *this;
-    }
-
-    // Move assignment.
-
-    variant& operator=(variant&& x) {
-        if (which_!=x.which_) {
-            variant_dynamic_impl<T...>::destroy(which_, data);
-            which_ = npos;
-            if (x.which_!=npos) {
-                variant_dynamic_impl<T...>::move(x.which_, data, x.data);
-                which_ = x.which_;
-            }
-        }
-        else {
-            if (x.which_!=npos) {
-                variant_dynamic_impl<T...>::move_assign(x.which_, data, x.data);
-            }
-        }
-        return *this;
-    }
-
-    // In place construction.
-
-    template <std::size_t I,
-              typename... Args,
-              typename = std::enable_if_t<(I<sizeof...(T))>,
-              typename X = type_select_t<I, T...>,
-              typename = std::enable_if_t<std::is_constructible<X, Args...>::value>>
-    X& emplace(Args&&... args) {
-        if (which_!=npos) {
-            variant_dynamic_impl<T...>::destroy(which_, data);
-            which_ = npos;
-        }
-        new(data_ptr<X>()) X(std::forward<Args>(args)...);
-        return *data_ptr<X>();
-    }
-
-    template <std::size_t I,
-              typename U,
-              typename... Args,
-              typename = std::enable_if_t<(I<sizeof...(T))>,
-              typename X = type_select_t<I, T...>,
-              typename = std::enable_if_t<std::is_constructible<X, std::initializer_list<U>, Args...>::value>>
-    X& emplace(std::initializer_list<U> il, Args&&... args) {
-        if (which_!=npos) {
-            variant_dynamic_impl<T...>::destroy(which_, data);
-            which_ = npos;
-        }
-        new(data_ptr<X>()) X(il, std::forward<Args>(args)...);
-        which_ = I;
-        return *data_ptr<X>();
-    }
-
-    template <typename X,
-              typename... Args,
-              std::size_t I = type_index<X, T...>::value>
-    X& emplace(Args&&... args) {
-        return emplace<I>(std::forward<Args>(args)...);
-    }
-
-    template <typename X,
-              typename U,
-              typename... Args,
-              std::size_t I = type_index<X, T...>::value>
-    X& emplace(std::initializer_list<U> il, Args&&... args) {
-        return emplace<I>(il, std::forward<Args>(args)...);
-    }
-
-    // Swap.
-
-    void swap(variant& rhs) {
-        if (which_==rhs.which_) {
-            if (which_!=npos) {
-                variant_dynamic_impl<T...>::swap(which_, data, rhs.data);
-            }
-        }
-        else {
-            variant tmp(std::move(rhs));
-            rhs = std::move(*this);
-            *this = std::move(tmp);
-        }
-    }
-
-    // Queries.
-
-    std::size_t index() const { return which_; }
-
-    bool valueless_by_exception() const { return which_==npos; }
-
-    // Pointer access (does not throw).
-
-    template <std::size_t I, typename = std::enable_if_t<(I<sizeof...(T))>, typename X = type_select_t<I, T...>>
-    X* get_if() noexcept { return which_==I? data_ptr<X>(): nullptr; }
-
-    template <typename X, std::size_t I = type_index<X, T...>::value>
-    auto get_if() noexcept { return get_if<I>(); }
-
-    template <std::size_t I, typename = std::enable_if_t<(I<sizeof...(T))>, typename X = type_select_t<I, T...>>
-    const X* get_if() const noexcept { return which_==I? data_ptr<X>(): nullptr; }
-
-    template <typename X, std::size_t I = type_index<X, T...>::value>
-    auto get_if() const noexcept { return get_if<I>(); }
-
-    // Reference access (throws).
-
-    template <std::size_t I, typename = std::enable_if_t<(I<sizeof...(T))>>
-    auto& get() & {
-        if (auto* p = get_if<I>()) return *p;
-        else throw bad_variant_access{};
-    }
-
-    template <std::size_t I, typename = std::enable_if_t<(I<sizeof...(T))>>
-    auto& get() const & {
-        if (auto* p = get_if<I>()) return *p;
-        else throw bad_variant_access{};
-    }
-
-    template <std::size_t I, typename = std::enable_if_t<(I<sizeof...(T))>>
-    auto&& get() && {
-        if (auto* p = get_if<I>()) return std::move(*p);
-        else throw bad_variant_access{};
-    }
-
-    template <std::size_t I, typename = std::enable_if_t<(I<sizeof...(T))>>
-    auto&& get() const && {
-        if (auto* p = get_if<I>()) return std::move(*p);
-        else throw bad_variant_access{};
-    }
-
-    template <typename X, std::size_t I = type_index<X, T...>::value>
-    decltype(auto) get() { return get<I>(); }
-
-    template <typename X, std::size_t I = type_index<X, T...>::value>
-    decltype(auto) get() const { return get<I>(); }
-
-    // Comparisons.
-
-    bool operator==(const variant& x) const {
-        return which_==x.which_ && (valueless_by_exception() || variant_dynamic_impl<T...>::cmp_eq(which_, data, x.data));
-    }
-
-    bool operator!=(const variant& x) const {
-        return which_!=x.which_ || (!valueless_by_exception() && variant_dynamic_impl<T...>::cmp_ne(which_, data, x.data));
-    }
-};
-
-template <std::size_t I, std::size_t N>
-struct variant_visit {
-    template <typename R, typename Visitor, typename Variant>
-    static R visit(std::size_t i, Visitor&& f, Variant&& v) {
-        if (i==I) {
-            return static_cast<R>(std::forward<Visitor>(f)(std::forward<Variant>(v).template get<I>()));
-        }
-        else {
-            return variant_visit<I+1, N>::template visit<R>(i, std::forward<Visitor>(f), std::forward<Variant>(v));
-        }
-    }
-};
-
-template <std::size_t I>
-struct variant_visit<I, I> {
-    template <typename R, typename Visitor, typename Variant>
-    static R visit(std::size_t i, Visitor&& f, Variant&& v) {
-        throw bad_variant_access{}; // Actually, should never get here.
-    }
-};
-
-template <typename X> struct variant_size_impl;
-template <typename... T>
-struct variant_size_impl<variant<T...>>: std::integral_constant<std::size_t, sizeof...(T)> {};
-
-template <std::size_t I, typename T> struct variant_alternative;
-
-template <std::size_t I, typename... T>
-struct variant_alternative<I, variant<T...>> { using type = type_select_t<I, T...>; };
-
-template <std::size_t I, typename... T>
-struct variant_alternative<I, const variant<T...>> { using type = std::add_const_t<type_select_t<I, T...>>; };
-
-} // namespace detail
-
-template <typename... T>
-using variant = detail::variant<T...>;
-
-template <typename X>
-using variant_size = detail::variant_size_impl<std::remove_cv_t<std::remove_reference_t<X>>>;
-
-template <std::size_t I, typename V>
-using variant_alternative_t = typename detail::variant_alternative<I, V>::type;
-
-// util:: variants of std::get
-
-template <typename X, typename... T>
-decltype(auto) get(variant<T...>& v) { return v.template get<X>(); }
-
-template <typename X, typename... T>
-decltype(auto) get(const variant<T...>& v) { return v.template get<X>(); }
-
-template <typename X, typename... T>
-decltype(auto) get(variant<T...>&& v) { return std::move(v).template get<X>(); }
-
-template <typename X, typename... T>
-decltype(auto) get(const variant<T...>&& v) { return std::move(v).template get<X>(); }
-
-template <std::size_t I, typename... T>
-decltype(auto) get(variant<T...>& v) { return v.template get<I>(); }
-
-template <std::size_t I, typename... T>
-decltype(auto) get(const variant<T...>& v) { return v.template get<I>(); }
-
-template <std::size_t I, typename... T>
-decltype(auto) get(variant<T...>&& v) { return std::move(v).template get<I>(); }
-
-template <std::size_t I, typename... T>
-decltype(auto) get(const variant<T...>&& v) { return std::move(v).template get<I>(); }
-
-// util:: variants of std::get_if
-
-template <typename X, typename... T>
-decltype(auto) get_if(variant<T...>& v) noexcept { return v.template get_if<X>(); }
-
-template <typename X, typename... T>
-decltype(auto) get_if(const variant<T...>& v) noexcept { return v.template get_if<X>(); }
-
-template <std::size_t I, typename... T>
-decltype(auto) get_if(variant<T...>& v) noexcept { return v.template get_if<I>(); }
-
-template <std::size_t I, typename... T>
-decltype(auto) get_if(const variant<T...>& v) noexcept { return v.template get_if<I>(); }
-
-// One-argument visitor
-
-template <typename Visitor, typename Variant>
-decltype(auto) visit(Visitor&& f, Variant&& v) {
-    using R = decltype(f(get<0>(std::forward<Variant>(v))));
-
-    if (v.valueless_by_exception()) throw bad_variant_access{};
-    std::size_t i = v.index();
-    return static_cast<R>(detail::variant_visit<0, variant_size<Variant>::value>::template visit<R>(i,
-        std::forward<Visitor>(f), std::forward<Variant>(v)));
-}
-
-template <typename R, typename Visitor, typename Variant>
-R visit(Visitor&& f, Variant&& v) {
-    if (v.valueless_by_exception()) throw bad_variant_access{};
-    std::size_t i = v.index();
-    return static_cast<R>(detail::variant_visit<0, variant_size<Variant>::value>::template visit<R>(i,
-        std::forward<Visitor>(f), std::forward<Variant>(v)));
-}
-
-// Not implementing multi-argument visitor yet!
-// (If we ever have a use case...)
-
-} // namespace util
-} // namespace arb
-
-namespace std {
-
-// Unambitious hash:
-template <typename... T>
-struct hash<::arb::util::variant<T...>> {
-    std::size_t operator()(const ::arb::util::variant<T...>& v) const {
-        return v.index() ^
-            ::arb::util::visit([](const auto& a) { return std::hash<std::remove_cv_t<std::remove_reference_t<decltype(a)>>>{}(a); }, v);
-    }
-};
-
-// Still haven't really determined if it is okay to have a variant<>, but if we do allow it...
-template <>
-struct hash<::arb::util::variant<>> {
-    std::size_t operator()(const ::arb::util::variant<>& v) const { return 0u; };
-};
-
-// std::swap specialization.
-template <typename... T>
-void swap(::arb::util::variant<T...>& v1, ::arb::util::variant<T...>& v2) {
-    v1.swap(v2);
-}
-} // namespace std
diff --git a/arbor/mc_cell_group.cpp b/arbor/mc_cell_group.cpp
index 2b6950d2e0bbce24be3205e988d2ba22f1e9e1fb..124734bec01cf29e6ad13bba9c0fd09289ae1ac4 100644
--- a/arbor/mc_cell_group.cpp
+++ b/arbor/mc_cell_group.cpp
@@ -1,5 +1,6 @@
 #include <functional>
 #include <unordered_set>
+#include <variant>
 #include <vector>
 
 #include <arbor/assert.hpp>
@@ -309,7 +310,7 @@ void run_samples(
     std::vector<sample_record>& sample_records,
     fvm_probe_scratch& scratch)
 {
-    util::visit([&](auto& x) {run_samples(x, sc, raw_times, raw_samples, sample_records, scratch); }, sc.pdata_ptr->info);
+    std::visit([&](auto& x) {run_samples(x, sc, raw_times, raw_samples, sample_records, scratch); }, sc.pdata_ptr->info);
 }
 
 void mc_cell_group::advance(epoch ep, time_type dt, const event_lane_subrange& event_lanes) {
diff --git a/arbor/spike_source_cell_group.cpp b/arbor/spike_source_cell_group.cpp
index c5521b306f14ba6cd4f1cbb4f936a74b665d8853..0e3b3ba86ab54265894e2a2ca493b496b3e94e64 100644
--- a/arbor/spike_source_cell_group.cpp
+++ b/arbor/spike_source_cell_group.cpp
@@ -21,7 +21,7 @@ spike_source_cell_group::spike_source_cell_group(const std::vector<cell_gid_type
             auto cell = util::any_cast<spike_source_cell>(rec.get_cell_description(gid));
             time_sequences_.push_back(std::move(cell.seq));
         }
-        catch (util::bad_any_cast& e) {
+        catch (std::bad_any_cast& e) {
             throw bad_cell_description(cell_kind::spike_source, gid);
         }
     }
diff --git a/arbor/util/sentinel.hpp b/arbor/util/sentinel.hpp
index 30d0696d72502c65d6191c22c2c1f448efafb9ed..22e46ffc622f0f41edc0253cfa2708dd66391da0 100644
--- a/arbor/util/sentinel.hpp
+++ b/arbor/util/sentinel.hpp
@@ -1,8 +1,7 @@
 #pragma once
 
 #include <type_traits>
-
-#include <arbor/util/variant.hpp>
+#include <variant>
 
 #include "util/meta.hpp"
 
@@ -28,7 +27,7 @@ struct iterator_category_select<I,S,true> {
 
 template <typename I, typename S>
 class sentinel_iterator {
-    arb::util::variant<I, S> e_;
+    std::variant<I, S> e_;
 
     I& iter() {
         arb_assert(!is_sentinel());
diff --git a/doc/cpp_common.rst b/doc/cpp_common.rst
index ff4d1273affb25caab484067dc814152add6918c..0604b98746e75562a5d7f9e92b735c4698816bbd 100644
--- a/doc/cpp_common.rst
+++ b/doc/cpp_common.rst
@@ -112,7 +112,7 @@ Probes
 
            Opaque key, returned in sample record.
 
-    .. cpp:member:: util::any address
+    .. cpp:member:: std::any address
 
            Cell-type specific location info, specific to cell kind of ``id.gid``.
 
@@ -121,35 +121,31 @@ Utility Wrappers and Containers
 
 .. cpp:namespace:: arb::util
 
+.. cpp:class:: unique_any
 
-.. cpp:class:: template <typename T> optional
+   Equivalent to :cpp:class:`std::any`, except that:
 
-    A wrapper around a contained value of type :cpp:type:`T`, that may or may not be set.
-    A faithful copy of the C++17 ``std::optional`` type.
-    See the online C++ standard documentation
-    `<https://en.cppreference.com/w/cpp/utility/optional>`_
-    for more information.
+      * it can store any type that is move constructable;
+      * it is move only, that is, it can't be copied.
 
-.. cpp:class:: any
+.. cpp:class:: any_ptr
 
-    A container for a single value of any type that is copy constructable.
-    Used in the Arbor API where a type of a value passed to or from the API
-    is decided at run time.
+   Holds a pointer to an arbitrary type, together with the type information.
 
-    A faithful copy of the C++17 ``std::any`` type.
-    See the online C++ standard documentation
-    `<https://en.cppreference.com/w/cpp/utility/any>`_
-    for more information.
+   .. cpp:function:: template <typename T> T as()
 
-    The :cpp:any:`arb::util` namespace also implementations of the
-    :cpp:any:`any_cast`, :cpp:any:`make_any` and :cpp:any:`bad_any_cast`
-    helper functions and types from C++17.
+      Retrieve the pointer as type T. If T is ``void *`` or the same
+      as the type of the pointer stored in ``any_ptr``, return the held
+      value, cast accordingly. Otherwise return ``nullptr``.
 
-.. cpp:class:: unique_any
+   ``any_ptr`` can be used with ``util::any_cast``, so that
+   ``util::any_cast<T>(p)`` is equivalent to ``p.as<T>()`` for a value ``p``
+   of type ``any_ptr``.
 
-   Equivalent to :cpp:class:`util::any`, except that:
-   
-      * it can store any type that is move constructable;
-      * it is move only, that is it can't be copied.
+.. cpp:function:: template <typename T> any_cast(...)
+
+    Equivalent to ``std::any_cast`` for ``std::any`` arguments, ``any_cast``
+    also performs analagous casting for the :cpp:class:`unique_any` and
+    :cpp:class:`any_ptr` utility classes.
 
 
diff --git a/doc/cpp_recipe.rst b/doc/cpp_recipe.rst
index b08f9ae317a3edd30ed31d233e7a039998b29912..300830e03472cc5348d509c00e4e624fc732b344 100644
--- a/doc/cpp_recipe.rst
+++ b/doc/cpp_recipe.rst
@@ -133,7 +133,7 @@ Class Documentation
         By default throws :cpp:type:`std::logic_error`. If :cpp:func:`num_probes`
         returns a non-zero value, this must also be overridden.
 
-    .. cpp:function:: virtual util::any get_global_properties(cell_kind) const
+    .. cpp:function:: virtual std::any get_global_properties(cell_kind) const
 
         Global property type will be specific to given cell kind.
 
diff --git a/example/dryrun/dryrun.cpp b/example/dryrun/dryrun.cpp
index 53b5ce902cef34d8b87288eab7c00f8432fdc1dd..0ef5193d1aa5038ace8653cc9ee2177a3ee4fdb3 100644
--- a/example/dryrun/dryrun.cpp
+++ b/example/dryrun/dryrun.cpp
@@ -3,6 +3,7 @@
  *
  */
 
+#include <any>
 #include <cassert>
 #include <fstream>
 #include <iomanip>
@@ -80,7 +81,7 @@ public:
         return cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         arb::cable_cell_global_properties gprop;
         gprop.default_parameters = arb::neuron_parameter_defaults;
         return gprop;
diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp
index 69c95b882a7cc7be017f5b243dbb1d2cd0384add..f0c3e4b38a8d73c94a299e09730cf82e0572c52f 100644
--- a/example/gap_junctions/gap_junctions.cpp
+++ b/example/gap_junctions/gap_junctions.cpp
@@ -3,6 +3,7 @@
  *
  */
 
+#include <any>
 #include <fstream>
 #include <iomanip>
 #include <iostream>
@@ -99,7 +100,7 @@ public:
         return {arb::cable_probe_membrane_voltage{loc}};
     }
 
-    arb::util::any get_global_properties(cell_kind k) const override {
+    std::any get_global_properties(cell_kind k) const override {
         arb::cable_cell_global_properties a;
         a.default_parameters = arb::neuron_parameter_defaults;
         a.default_parameters.temperature_K = 308.15;
diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp
index 66121c07b473e981bc528ce7e01dc7ae73eea2f8..48147fc064794ba181877f1956e437d99cf04711 100644
--- a/example/generators/generators.cpp
+++ b/example/generators/generators.cpp
@@ -6,6 +6,7 @@
  * event generators, one inhibitory, and one excitatory, are attached.
  */
 
+#include <any>
 #include <cassert>
 #include <fstream>
 #include <iomanip>
@@ -70,7 +71,7 @@ public:
         return cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         arb::cable_cell_global_properties gprop;
         gprop.default_parameters = arb::neuron_parameter_defaults;
         return gprop;
diff --git a/example/lfp/lfp.cpp b/example/lfp/lfp.cpp
index 38217129f7f8556d17846f4441634a8424ee46a3..54520d5c21e6f2927ab9b3ea4c15bcce268a845f 100644
--- a/example/lfp/lfp.cpp
+++ b/example/lfp/lfp.cpp
@@ -1,3 +1,4 @@
+#include <any>
 #include <cassert>
 #include <vector>
 #include <iostream>
@@ -11,12 +12,14 @@
 #include <arbor/sampling.hpp>
 #include <arbor/simple_sampler.hpp>
 #include <arbor/simulation.hpp>
-#include <arbor/util/any.hpp>
+#include <arbor/util/any_cast.hpp>
 #include <arbor/util/any_ptr.hpp>
+#include <arbor/util/unique_any.hpp>
 
-using arb::util::any;
+using std::any;
 using arb::util::any_cast;
 using arb::util::any_ptr;
+using arb::util::unique_any;
 using arb::cell_gid_type;
 using arb::cell_member_type;
 
@@ -49,7 +52,7 @@ struct lfp_demo_recipe: public arb::recipe {
         return arb::cell_kind::cable;
     }
 
-    arb::util::unique_any get_cell_description(cell_gid_type) const override {
+    unique_any get_cell_description(cell_gid_type) const override {
         return cell_;
     }
 
diff --git a/example/probe-demo/probe-demo.cpp b/example/probe-demo/probe-demo.cpp
index 0598f02888843cbadde9649f529c7efd1e73c68e..860ffbc64c4c8529d2659a56718d8713272d95bb 100644
--- a/example/probe-demo/probe-demo.cpp
+++ b/example/probe-demo/probe-demo.cpp
@@ -1,3 +1,4 @@
+#include <any>
 #include <functional>
 #include <iomanip>
 #include <iostream>
@@ -13,14 +14,14 @@
 #include <arbor/morph/region.hpp>
 #include <arbor/simulation.hpp>
 #include <arbor/sampling.hpp>
-#include <arbor/util/any.hpp>
+#include <arbor/util/any_cast.hpp>
 #include <arbor/util/any_ptr.hpp>
 #include <tinyopt/smolopt.h>
 
 // Simulate a cell modelled as a simple cable with HH dynamics,
 // emitting the results of a user specified probe over time.
 
-using arb::util::any;
+using std::any;
 using arb::util::any_cast;
 using arb::util::any_ptr;
 
@@ -108,7 +109,7 @@ struct cable_recipe: public arb::recipe {
         return arb::cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    any get_global_properties(arb::cell_kind) const override {
         return gprop;
     }
 
diff --git a/example/ring/ring.cpp b/example/ring/ring.cpp
index 0a2137988ad56c9dca71c828e5827af25b5858e9..ed2d7a020a68e1c35564f3a8d8b645b947caa9a0 100644
--- a/example/ring/ring.cpp
+++ b/example/ring/ring.cpp
@@ -3,6 +3,7 @@
  *
  */
 
+#include <any>
 #include <cassert>
 #include <fstream>
 #include <iomanip>
@@ -115,7 +116,7 @@ public:
         return {arb::cable_probe_membrane_voltage{loc}};
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         return gprop_;
     }
 
diff --git a/example/single/single.cpp b/example/single/single.cpp
index 920849e3c8d87336811def28d0752ad1ee7e5dfd..bbcb65569d364a03c0253df0e76d1a1c06a4bf61 100644
--- a/example/single/single.cpp
+++ b/example/single/single.cpp
@@ -1,3 +1,4 @@
+#include <any>
 #include <fstream>
 #include <iomanip>
 #include <iostream>
@@ -50,7 +51,7 @@ struct single_recipe: public arb::recipe {
         return arb::cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         return gprop;
     }
 
diff --git a/python/cells.cpp b/python/cells.cpp
index cc867f6a5a368f0ec7efe9ebebea88843b9503e3..6f1b67f1f227376ff2e1dbc89738bd640fe4f96e 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -1,3 +1,12 @@
+#include <algorithm>
+#include <any>
+#include <cstddef>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
@@ -10,7 +19,8 @@
 #include <arbor/morph/segment_tree.hpp>
 #include <arbor/schedule.hpp>
 #include <arbor/spike_source_cell.hpp>
-#include <arbor/util/any.hpp>
+#include <arbor/util/any_cast.hpp>
+#include <arbor/util/optional.hpp>
 #include <arbor/util/unique_any.hpp>
 
 #include "cells.hpp"
@@ -20,6 +30,8 @@
 #include "schedule.hpp"
 #include "strprintf.hpp"
 
+using arb::util::any_cast;
+
 namespace pyarb {
 
 // Convert a cell description inside a Python object to a cell
@@ -104,12 +116,12 @@ struct label_dict_proxy {
                 throw std::string(result.error().message);
             }
             else if (result->type()==typeid(arb::region)) { // describes a region.
-                dict.set(name, std::move(arb::util::any_cast<arb::region&>(*result)));
+                dict.set(name, std::move(any_cast<arb::region&>(*result)));
                 auto it = std::lower_bound(regions.begin(), regions.end(), name);
                 if (it==regions.end() || *it!=name) regions.insert(it, name);
             }
             else if (result->type()==typeid(arb::locset)) { // describes a locset.
-                dict.set(name, std::move(arb::util::any_cast<arb::locset&>(*result)));
+                dict.set(name, std::move(any_cast<arb::locset&>(*result)));
                 auto it = std::lower_bound(locsets.begin(), locsets.end(), name);
                 if (it==locsets.end() || *it!=name) locsets.insert(it, name);
             }
diff --git a/python/flat_cell_builder.cpp b/python/flat_cell_builder.cpp
index 56a0c65e0508b48f2777c8af50a2d296e41b285c..2a2a3ca654b06ea4a5cbab72711910a7c953f30d 100644
--- a/python/flat_cell_builder.cpp
+++ b/python/flat_cell_builder.cpp
@@ -1,3 +1,9 @@
+#include <mutex>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
@@ -5,6 +11,7 @@
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/segment_tree.hpp>
+#include <arbor/util/any_cast.hpp>
 
 #include "conversion.hpp"
 #include "error.hpp"
@@ -12,6 +19,8 @@
 #include "s_expr.hpp"
 #include "strprintf.hpp"
 
+using arb::util::any_cast;
+
 namespace pyarb {
 
 class flat_cell_builder {
@@ -96,7 +105,7 @@ public:
                 if (dict_.locset(name)) {
                     throw pyarb_error("Region name clashes with a locset.");
                 }
-                auto& reg = arb::util::any_cast<arb::region&>(*result);
+                auto& reg = any_cast<arb::region&>(*result);
                 if (auto r = dict_.region(name)) {
                     dict_.set(name, join(std::move(reg), std::move(*r)));
                 }
@@ -108,7 +117,7 @@ public:
                 if (dict_.region(name)) {
                     throw pyarb_error("Locset name clashes with a region.");
                 }
-                auto& loc = arb::util::any_cast<arb::locset&>(*result);
+                auto& loc = any_cast<arb::locset&>(*result);
                 if (auto l = dict_.locset(name)) {
                     dict_.set(name, sum(std::move(loc), std::move(*l)));
                 }
diff --git a/python/morph_parse.cpp b/python/morph_parse.cpp
index 2df63fc59bde458b682b8f923fa2a5194b69db47..33caa33eac9a331055b27e229b4c9091194f9e91 100644
--- a/python/morph_parse.cpp
+++ b/python/morph_parse.cpp
@@ -1,12 +1,15 @@
-#include <arbor/util/any.hpp>
+#include <any>
+#include <limits>
+
 #include <arbor/morph/region.hpp>
 #include <arbor/morph/locset.hpp>
-#include <limits>
 
 #include "error.hpp"
 #include "s_expr.hpp"
 #include "morph_parse.hpp"
 
+using std::any_cast;
+
 namespace pyarb {
 
 struct nil_tag {};
@@ -32,40 +35,40 @@ bool match<arb::locset>(const std::type_info& info) {
 }
 
 template <typename T>
-T eval_cast(arb::util::any arg) {
-    return std::move(arb::util::any_cast<T&>(arg));
+T eval_cast(std::any arg) {
+    return std::move(any_cast<T&>(arg));
 }
 
 template <>
-double eval_cast<double>(arb::util::any arg) {
-    if (arg.type()==typeid(int)) return arb::util::any_cast<int>(arg);
-    return arb::util::any_cast<double>(arg);
+double eval_cast<double>(std::any arg) {
+    if (arg.type()==typeid(int)) return any_cast<int>(arg);
+    return any_cast<double>(arg);
 }
 
 template <>
-arb::region eval_cast<arb::region>(arb::util::any arg) {
-    if (arg.type()==typeid(arb::region)) return arb::util::any_cast<arb::region>(arg);
+arb::region eval_cast<arb::region>(std::any arg) {
+    if (arg.type()==typeid(arb::region)) return any_cast<arb::region>(arg);
     return arb::reg::nil();
 }
 
 template <>
-arb::locset eval_cast<arb::locset>(arb::util::any arg) {
-    if (arg.type()==typeid(arb::locset)) return arb::util::any_cast<arb::locset>(arg);
+arb::locset eval_cast<arb::locset>(std::any arg) {
+    if (arg.type()==typeid(arb::locset)) return any_cast<arb::locset>(arg);
     return arb::ls::nil();
 }
 
 template <typename... Args>
 struct call_eval {
-    using ftype = std::function<arb::util::any(Args...)>;
+    using ftype = std::function<std::any(Args...)>;
     ftype f;
     call_eval(ftype f): f(std::move(f)) {}
 
     template<std::size_t... I>
-    arb::util::any expand_args_then_eval(std::vector<arb::util::any> args, std::index_sequence<I...>) {
+    std::any expand_args_then_eval(std::vector<std::any> args, std::index_sequence<I...>) {
         return f(eval_cast<Args>(std::move(args[I]))...);
     }
 
-    arb::util::any operator()(std::vector<arb::util::any> args) {
+    std::any operator()(std::vector<std::any> args) {
         return expand_args_then_eval(std::move(args), std::make_index_sequence<sizeof...(Args)>());
     }
 };
@@ -73,21 +76,21 @@ struct call_eval {
 template <typename... Args>
 struct call_match {
     template <std::size_t I, typename T, typename Q, typename... Rest>
-    bool match_args_impl(const std::vector<arb::util::any>& args) const {
+    bool match_args_impl(const std::vector<std::any>& args) const {
         return match<T>(args[I].type()) && match_args_impl<I+1, Q, Rest...>(args);
     }
 
     template <std::size_t I, typename T>
-    bool match_args_impl(const std::vector<arb::util::any>& args) const {
+    bool match_args_impl(const std::vector<std::any>& args) const {
         return match<T>(args[I].type());
     }
 
     template <std::size_t I>
-    bool match_args_impl(const std::vector<arb::util::any>& args) const {
+    bool match_args_impl(const std::vector<std::any>& args) const {
         return true;
     }
 
-    bool operator()(const std::vector<arb::util::any>& args) const {
+    bool operator()(const std::vector<std::any>& args) const {
         const auto nargs_in = args.size();
         const auto nargs_ex = sizeof...(Args);
         return nargs_in==nargs_ex? match_args_impl<0, Args...>(args): false;
@@ -99,7 +102,7 @@ struct fold_eval {
     using fold_fn = std::function<T(T, T)>;
     fold_fn f;
 
-    using anyvec = std::vector<arb::util::any>;
+    using anyvec = std::vector<std::any>;
     using iterator = anyvec::iterator;
 
     fold_eval(fold_fn f): f(std::move(f)) {}
@@ -111,14 +114,14 @@ struct fold_eval {
         return f(eval_cast<T>(std::move(*left)), fold_impl(left+1, right));
     }
 
-    arb::util::any operator()(anyvec args) {
+    std::any operator()(anyvec args) {
         return fold_impl(args.begin(), args.end());
     }
 };
 
 template <typename T>
 struct fold_match {
-    using anyvec = std::vector<arb::util::any>;
+    using anyvec = std::vector<std::any>;
     bool operator()(const anyvec& args) const {
         if (args.size()<2u) return false;
         for (auto& a: args) {
@@ -129,8 +132,8 @@ struct fold_match {
 };
 
 struct evaluator {
-    using any_vec = std::vector<arb::util::any>;
-    using eval_fn = std::function<arb::util::any(any_vec)>;
+    using any_vec = std::vector<std::any>;
+    using eval_fn = std::function<std::any(any_vec)>;
     using args_fn = std::function<bool(const any_vec&)>;
 
     eval_fn eval;
@@ -245,12 +248,12 @@ std::unordered_multimap<std::string, evaluator> eval_map {
                             "'sum' with at least 2 arguments: (locset locset [...locset])")},
 };
 
-parse_hopefully<arb::util::any> eval(const s_expr& e);
+parse_hopefully<std::any> eval(const s_expr& e);
 
-parse_hopefully<std::vector<arb::util::any>> eval_args(const s_expr& e) {
-    if (!e) return {std::vector<arb::util::any>{}}; // empty argument list
+parse_hopefully<std::vector<std::any>> eval_args(const s_expr& e) {
+    if (!e) return {std::vector<std::any>{}}; // empty argument list
     const s_expr* h = &e;
-    std::vector<arb::util::any> args;
+    std::vector<std::any> args;
     while (*h) {
         auto arg = eval(h->head());
         if (!arg) return std::move(arg.error());
@@ -267,7 +270,7 @@ parse_hopefully<std::vector<arb::util::any>> eval_args(const s_expr& e) {
 //  'cat' with 3 arguments: (locset region integer)
 // Where 'foo', 'bar' and 'cat' are the name of the function, and the
 // types (integer, real, region, locset) are inferred from the arguments.
-std::string eval_description(const char* name, const std::vector<arb::util::any>& args) {
+std::string eval_description(const char* name, const std::vector<std::any>& args) {
     auto type_string = [](const std::type_info& t) -> const char* {
         if (t==typeid(int))         return "integer";
         if (t==typeid(double))      return "real";
@@ -305,7 +308,7 @@ std::string eval_description(const char* name, const std::vector<arb::util::any>
 // a parse_error_state with an error string and location.
 //
 // If there was an unexpected/fatal error, an exception will be thrown.
-parse_hopefully<arb::util::any> eval(const s_expr& e) {
+parse_hopefully<std::any> eval(const s_expr& e) {
     if (e.is_atom()) {
         auto& t = e.atom();
         switch (t.kind) {
diff --git a/python/morph_parse.hpp b/python/morph_parse.hpp
index cb78cb57ee20f9f688e460201b70c44f3899d8f9..84dd1ba30645ddb531b98a093b69cb496e094ee0 100644
--- a/python/morph_parse.hpp
+++ b/python/morph_parse.hpp
@@ -1,10 +1,10 @@
 #pragma once
 
+#include <any>
+
 #include "error.hpp"
 #include "s_expr.hpp"
 
-#include <arbor/util/any.hpp>
-
 namespace pyarb {
 
 struct parse_error_state {
@@ -15,6 +15,6 @@ struct parse_error_state {
 template <typename T>
 using parse_hopefully = hopefully<T, parse_error_state>;
 
-parse_hopefully<arb::util::any> eval(const s_expr&);
+parse_hopefully<std::any> eval(const s_expr&);
 
 } // namespace pyarb
diff --git a/python/recipe.hpp b/python/recipe.hpp
index 51aa070ef4cafe07ff74881ed737021acd94c48e..bf421aad80948f5e4a28c39e4071deab5fc4d3b6 100644
--- a/python/recipe.hpp
+++ b/python/recipe.hpp
@@ -20,7 +20,7 @@ arb::probe_info cable_probe(std::string kind, arb::cell_member_type id, arb::mlo
 // pyarb::recipe is the recipe interface used by Python.
 // Calls that return generic types return pybind11::object, to avoid
 // having to wrap some C++ types used by the C++ interface (specifically
-// util::unique_any, util::any, std::unique_ptr, etc.)
+// util::unique_any, std::any, std::unique_ptr, etc.)
 // For example, requests for cell description return pybind11::object, instead
 // of util::unique_any used by the C++ recipe interface.
 // The py_recipe_shim unwraps the python objects, and forwards them
@@ -158,13 +158,13 @@ public:
     }
 
     // TODO: make thread safe
-    arb::util::any get_global_properties(arb::cell_kind kind) const override {
+    std::any get_global_properties(arb::cell_kind kind) const override {
         if (kind==arb::cell_kind::cable) {
             arb::cable_cell_global_properties gprop;
             gprop.default_parameters = arb::neuron_parameter_defaults;
             return gprop;
         }
-        return arb::util::any{};
+        return std::any{};
     }
 };
 
diff --git a/python/s_expr.cpp b/python/s_expr.cpp
index 6360713c3813b302662df47b0c69bef0fad482b7..e9db04781fb1d179c9b4d48f1fb4e7e9967af3cb 100644
--- a/python/s_expr.cpp
+++ b/python/s_expr.cpp
@@ -3,10 +3,10 @@
 #include <string>
 #include <memory>
 #include <ostream>
+#include <variant>
 #include <vector>
 
 #include <arbor/arbexcept.hpp>
-#include <arbor/util/variant.hpp>
 
 #include "s_expr.hpp"
 #include "strprintf.hpp"
@@ -300,23 +300,23 @@ bool s_expr::is_atom() const {
 }
 
 const token& s_expr::atom() const {
-    return state.get<0>();
+    return std::get<0>(state);
 }
 
 const s_expr& s_expr::head() const {
-    return state.get<1>().head.get();
+    return std::get<1>(state).head.get();
 }
 
 const s_expr& s_expr::tail() const {
-    return state.get<1>().tail.get();
+    return std::get<1>(state).tail.get();
 }
 
 s_expr& s_expr::head() {
-    return state.get<1>().head.get();
+    return std::get<1>(state).head.get();
 }
 
 s_expr& s_expr::tail() {
-    return state.get<1>().tail.get();
+    return std::get<1>(state).tail.get();
 }
 
 s_expr::operator bool() const {
diff --git a/python/s_expr.hpp b/python/s_expr.hpp
index bf6998a15ba413ccc5767e863fc571717624ef93..8b041d56a3d580c1952d90218ed81364c225de1b 100644
--- a/python/s_expr.hpp
+++ b/python/s_expr.hpp
@@ -2,10 +2,9 @@
 
 #include <string>
 #include <memory>
+#include <variant>
 #include <vector>
 
-#include <arbor/util/variant.hpp>
-
 namespace pyarb {
 
 enum class tok {
@@ -84,12 +83,12 @@ struct s_expr {
     // An s_expr can be one of
     //      1. an atom
     //      2. a pair of s_expr (head and tail)
-    // The s_expr uses a util::variant to represent these two possible states,
+    // The s_expr uses std::variant to represent these two possible states,
     // which requires using an incomplete definition of s_expr, which is stored
     // using a std::unique_ptr.
 
     using pair_type = s_pair<value_wrapper<s_expr>>;
-    arb::util::variant<token, pair_type> state = token{-1, tok::nil, "nil"};
+    std::variant<token, pair_type> state = token{-1, tok::nil, "nil"};
 
     s_expr(const s_expr& s): state(s.state) {}
     s_expr() = default;
diff --git a/python/single_cell_model.cpp b/python/single_cell_model.cpp
index 4e406d5afe6044cdec08da8a4e7ad08982a901fb..9c12ed4ce7cc7bf74913fbe09792b95fd41c4a23 100644
--- a/python/single_cell_model.cpp
+++ b/python/single_cell_model.cpp
@@ -1,4 +1,7 @@
+#include <any>
+#include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include <pybind11/pybind11.h>
@@ -8,10 +11,13 @@
 #include <arbor/load_balance.hpp>
 #include <arbor/recipe.hpp>
 #include <arbor/simulation.hpp>
+#include <arbor/util/any_cast.hpp>
 
 #include "error.hpp"
 #include "cells.hpp"
 
+using arb::util::any_cast;
+
 namespace pyarb {
 
 //
@@ -45,7 +51,7 @@ struct trace_callback {
     void operator()(arb::probe_metadata, std::size_t n, const arb::sample_record* recs) {
         // Push each (time, value) pair from the last epoch into trace_.
         for (std::size_t i=0; i<n; ++i) {
-            if (auto p = arb::util::any_cast<const double*>(recs[i].data)) {
+            if (auto p = any_cast<const double*>(recs[i].data)) {
                 trace_.t.push_back(recs[i].time);
                 trace_.v.push_back(*p);
             }
@@ -125,7 +131,7 @@ struct single_cell_recipe: arb::recipe {
         return {}; // No gap junctions on a single cell model.
     }
 
-    virtual arb::util::any get_global_properties(arb::cell_kind) const override {
+    virtual std::any get_global_properties(arb::cell_kind) const override {
         return gprop_;
     }
 };
diff --git a/python/test/cpp/s_expr.cpp b/python/test/cpp/s_expr.cpp
index 62300624a8f7c0f2ac8576fc1a59fc62afb29603..ced25e9ef2d81c29fd39ad57d4ae4141dd6ecc70 100644
--- a/python/test/cpp/s_expr.cpp
+++ b/python/test/cpp/s_expr.cpp
@@ -1,15 +1,20 @@
 #include "gtest.h"
 
+#include <string>
+
 #include <arbor/morph/region.hpp>
 #include <arbor/morph/locset.hpp>
+#include <arbor/util/any_cast.hpp>
 
-#include <morph_parse.hpp>
-#include <s_expr.hpp>
-#include <strprintf.hpp>
+#include "morph_parse.hpp"
+#include "s_expr.hpp"
+#include "strprintf.hpp"
 
 using namespace pyarb;
 using namespace std::string_literals;
 
+using arb::util::any_cast;
+
 TEST(s_expr, identifier) {
     EXPECT_TRUE(test_identifier("foo"));
     EXPECT_TRUE(test_identifier("f1"));
@@ -70,11 +75,11 @@ TEST(s_expr, atoms) {
 TEST(s_expr, parse) {
     auto round_trip_reg = [](const char* in) {
         auto x = eval(parse(in));
-        return util::pprintf("{}", arb::util::any_cast<arb::region>(*x));
+        return util::pprintf("{}", any_cast<arb::region>(*x));
     };
     auto round_trip_loc = [](const char* in) {
         auto x = eval(parse(in));
-        return util::pprintf("{}", arb::util::any_cast<arb::locset>(*x));
+        return util::pprintf("{}", any_cast<arb::locset>(*x));
     };
 
     EXPECT_EQ("(cable 3 0 1)",      round_trip_reg("(branch 3)"));
@@ -86,8 +91,8 @@ TEST(s_expr, parse) {
     EXPECT_EQ("(root)",     round_trip_loc("(root)"));
     EXPECT_EQ("(locset \"cat_burgler\")", round_trip_loc("(locset \"cat_burgler\")"));
 
-    auto lhs = arb::util::any_cast<arb::region>(*eval(parse("(region \"dend\")")));
-    auto rhs = arb::util::any_cast<arb::region>(*eval(parse("(all)")));
+    auto lhs = any_cast<arb::region>(*eval(parse("(region \"dend\")")));
+    auto rhs = any_cast<arb::region>(*eval(parse("(all)")));
 
     EXPECT_EQ(util::pprintf("{}", join(lhs,rhs)), "(join (region \"dend\") (all))");
 }
@@ -95,7 +100,7 @@ TEST(s_expr, parse) {
 TEST(s_expr, comments) {
     auto round_trip_reg = [](const char* in) {
         auto x = eval(parse(in));
-        return util::pprintf("{}", arb::util::any_cast<arb::region>(*x));
+        return util::pprintf("{}", any_cast<arb::region>(*x));
     };
 
     EXPECT_EQ("(all)",  round_trip_reg("(all) ; a comment"));
diff --git a/test/simple_recipes.hpp b/test/simple_recipes.hpp
index 24051eb005951838cac5e424f5d377563905ac47..9a1c1dad7d324d8792b8cc78935ca3881043b7ce 100644
--- a/test/simple_recipes.hpp
+++ b/test/simple_recipes.hpp
@@ -2,6 +2,7 @@
 
 // Simple recipe classes for use in unit and validation tests.
 
+#include <any>
 #include <unordered_map>
 #include <vector>
 
@@ -9,6 +10,7 @@
 #include <arbor/cable_cell.hpp>
 #include <arbor/cable_cell_param.hpp>
 #include <arbor/recipe.hpp>
+#include <arbor/util/unique_any.hpp>
 
 namespace arb {
 
@@ -29,16 +31,16 @@ public:
         return probes_.at(i);
     }
 
-    virtual void add_probe(cell_gid_type gid, probe_tag tag, util::any address) {
+    virtual void add_probe(cell_gid_type gid, probe_tag tag, std::any address) {
         probes_[gid].emplace_back(std::move(address), tag);
     }
 
-    util::any get_global_properties(cell_kind k) const override {
+    std::any get_global_properties(cell_kind k) const override {
         switch (k) {
         case cell_kind::cable:
             return cell_gprop_;
         default:
-            return util::any{};
+            return std::any{};
         }
     }
 
diff --git a/test/ubench/CMakeLists.txt b/test/ubench/CMakeLists.txt
index e37f955e33f5d870f9813866839a6a629b828ced..381a8ffa0ca8d88884d36bfbd8fae472686bcb46 100644
--- a/test/ubench/CMakeLists.txt
+++ b/test/ubench/CMakeLists.txt
@@ -7,8 +7,8 @@ set(bench_sources
     default_construct.cpp
     event_setup.cpp
     event_binning.cpp
-    fvm_discretize.cpp
-    mech_vec.cpp
+    #    fvm_discretize.cpp
+    #    mech_vec.cpp
     task_system.cpp
 )
 
diff --git a/test/ubench/mech_vec.cpp b/test/ubench/mech_vec.cpp
index 00074224969deee7f1eb541672789b1c6306ebda..ed45e38f5c50d342b81678d9f5af1975b2611d9a 100644
--- a/test/ubench/mech_vec.cpp
+++ b/test/ubench/mech_vec.cpp
@@ -5,6 +5,7 @@
 // NOTE: This targets an earlier version of the Arbor API and
 // will need to be reworked in order to compile.
 
+#include <any>
 #include <fstream>
 
 #include <arbor/cable_cell.hpp>
@@ -80,7 +81,7 @@ public:
         return cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         return gprop_;
     }
 };
@@ -123,7 +124,7 @@ public:
         return cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         return gprop_;
     }
 };
@@ -168,7 +169,7 @@ public:
         return cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         return gprop_;
     }
 };
@@ -211,7 +212,7 @@ public:
         return cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         return gprop_;
     }
 };
@@ -256,7 +257,7 @@ public:
         return cell_kind::cable;
     }
 
-    arb::util::any get_global_properties(arb::cell_kind) const override {
+    std::any get_global_properties(arb::cell_kind) const override {
         return gprop_;
     }
 };
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 43d4eda8dc5c15450f0a77b2d781ffdb01900b6c..cd9ee32d9f9d2c1080016ff0b3018573261f30e0 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -84,7 +84,8 @@ endforeach()
 set(unit_sources
     ../common_cells.cpp
     test_algorithms.cpp
-    test_any.cpp
+    test_any_cast.cpp
+    test_any_ptr.cpp
     test_any_visitor.cpp
     test_backend.cpp
     test_counter.cpp
@@ -159,7 +160,6 @@ set(unit_sources
     test_uninitialized.cpp
     test_unique.cpp
     test_unique_any.cpp
-    test_variant.cpp
     test_vector.cpp
     test_version.cpp
 
diff --git a/test/unit/test_any.cpp b/test/unit/test_any.cpp
deleted file mode 100644
index 5085bb1c3a545037cfb0bf78282008d2906ff522..0000000000000000000000000000000000000000
--- a/test/unit/test_any.cpp
+++ /dev/null
@@ -1,445 +0,0 @@
-#include <string>
-#include <type_traits>
-#include <typeinfo>
-
-#include <arbor/util/any.hpp>
-#include <arbor/util/any_ptr.hpp>
-
-#include "../gtest.h"
-#include "common.hpp"
-
-using namespace std::string_literals;
-using namespace arb;
-
-TEST(any, copy_construction) {
-    util::any any_int(2);
-    EXPECT_EQ(any_int.type(), typeid(int));
-
-    util::any any_float(2.0f);
-    EXPECT_EQ(any_float.type(), typeid(float));
-
-    std::string str = "hello";
-    util::any any_string(str);
-    EXPECT_EQ(any_string.type(), typeid(std::string));
-}
-
-namespace {
-
-    struct moveable {
-        moveable() = default;
-
-        moveable(moveable&& other):
-            moves(other.moves+1), copies(other.copies)
-        {}
-
-        moveable(const moveable& other):
-            moves(other.moves), copies(other.copies+1)
-        {}
-
-        int moves=0;
-        int copies=0;
-    };
-
-}
-
-TEST(any, move_construction) {
-    moveable m;
-
-    util::any copied(m);
-    util::any moved(std::move(m));
-
-    // Check that the expected number of copies and moves were performed.
-    // Use cast to reference to avoid copy or move of contained object.
-    const auto& cref = util::any_cast<moveable&>(copied);
-    EXPECT_EQ(cref.moves, 0);
-    EXPECT_EQ(cref.copies, 1);
-
-    const auto& mref = util::any_cast<moveable&>(moved);
-    EXPECT_EQ(mref.moves, 1);
-    EXPECT_EQ(mref.copies, 0);
-
-    // construction by any&& should not make any copies or moves of the
-    // constructed value
-    util::any fin(std::move(moved));
-    EXPECT_FALSE(moved.has_value()); // moved has been moved from and should be empty
-    const auto& fref = util::any_cast<moveable&>(fin);
-    EXPECT_EQ(fref.moves, 1);
-    EXPECT_EQ(fref.copies, 0);
-
-    const auto value = util::any_cast<moveable>(fin);
-    EXPECT_EQ(value.moves, 1);
-    EXPECT_EQ(value.copies, 1);
-}
-
-TEST(any, type) {
-    using util::any;
-
-    any anyi(42);
-    any anys("hello"s);
-    any anyv(std::vector<int>{1, 2, 3});
-    any any0;
-
-    EXPECT_EQ(typeid(int), anyi.type());
-    EXPECT_EQ(typeid(std::string), anys.type());
-    EXPECT_EQ(typeid(std::vector<int>), anyv.type());
-    EXPECT_EQ(typeid(void), any0.type());
-
-    anyi.reset();
-    EXPECT_EQ(typeid(void), anyi.type());
-
-    anyi = std::true_type();
-    EXPECT_EQ(typeid(std::true_type), anyi.type());
-}
-
-TEST(any, swap) {
-    using util::any;
-    using util::any_cast;
-
-    any any1(42);     // integer
-    any any2(3.14);   // double
-
-    EXPECT_EQ(typeid(int),    any1.type());
-    EXPECT_EQ(typeid(double), any2.type());
-
-    any1.swap(any2);
-
-    EXPECT_EQ(any_cast<int>(any2), 42);
-    EXPECT_EQ(any_cast<double>(any1), 3.14);
-
-    EXPECT_EQ(typeid(double), any1.type());
-    EXPECT_EQ(typeid(int),    any2.type());
-
-    any1.swap(any2);
-
-    EXPECT_EQ(any_cast<double>(any2), 3.14);
-    EXPECT_EQ(any_cast<int>(any1), 42);
-
-    EXPECT_EQ(typeid(int),    any1.type());
-    EXPECT_EQ(typeid(double), any2.type());
-}
-
-// These should fail at compile time if the constraint that the contents of any
-// satisfy CopyConstructable. This implementation is rock solid, so they have
-// to be commented out.
-/*
-TEST(any, not_copy_constructable) {
-    util::any a(testing::nocopy<int>(3));
-
-    testing::nocopy<int> x(3);
-    util::any b(std::move(x));
-}
-*/
-
-// test any_cast(any*)
-//   - these have different behavior to any_cast on reference types
-//   - are used by the any_cast on reference types
-TEST(any, any_cast_ptr) {
-
-    // test that valid pointers are returned for int and std::string types
-
-    util::any ai(42);
-    auto ptr_i = util::any_cast<int>(&ai);
-    EXPECT_EQ(*ptr_i, 42);
-
-    util::any as("hello"s);
-    auto ptr_s = util::any_cast<std::string>(&as);
-    EXPECT_EQ(*ptr_s, "hello");
-
-    // test that exceptions are thrown for invalid casts
-    EXPECT_EQ(util::any_cast<int>(&as), nullptr);
-    EXPECT_EQ(util::any_cast<std::string>(&ai), nullptr);
-    util::any empty;
-    EXPECT_EQ(util::any_cast<int>(&empty), nullptr);
-    EXPECT_EQ(util::any_cast<int>((util::any*)nullptr), nullptr);
-
-    // Check that constness of the returned pointer matches that the input.
-    {
-        util::any a(42);
-        auto p = util::any_cast<int>(&a);
-
-        // any_cast(any*) should not return const*
-        EXPECT_TRUE((std::is_same<int*, decltype(p)>::value));
-    }
-    {
-        const util::any a(42);
-        auto p = util::any_cast<int>(&a);
-
-        // any_cast(const any*) should return const*
-        EXPECT_TRUE((std::is_same<const int*, decltype(p)>::value));
-    }
-}
-
-TEST(any, any_cast_ref) {
-    {
-        util::any a(42);
-        auto i = util::any_cast<int>(a);
-        EXPECT_EQ(typeid(i), typeid(int));
-        EXPECT_EQ(i, 42);
-
-        // check that we can take a reference to the
-        // underlying storage
-        auto& r = util::any_cast<int&>(a);
-        r = 3;
-        EXPECT_EQ(3, util::any_cast<int>(a));
-        EXPECT_TRUE((std::is_same<int&, decltype(r)>::value));
-    }
-
-    {   // check that const references can be returned to const objects
-        const util::any a(42);
-        auto& r = util::any_cast<const int&>(a);
-        EXPECT_TRUE((std::is_same<const int&, decltype(r)>::value));
-
-        // The following should fail with a static assertion if uncommented, because
-        // it requests a non-const reference to a const object.
-        //auto& instafail = util::any_cast<int&>(a);
-    }
-}
-
-// test any_cast(any&&)
-TEST(any, any_cast_rvalue) {
-    auto moved = util::any_cast<moveable>(util::any(moveable()));
-    EXPECT_EQ(moved.moves, 2);
-    EXPECT_EQ(moved.copies, 0);
-}
-
-TEST(any, std_swap) {
-    util::any a1(42);
-    util::any a2(3.14);
-
-    auto pi = util::any_cast<int>(&a1);
-    auto pd = util::any_cast<double>(&a2);
-
-    std::swap(a1, a2);
-
-    // test that values were swapped
-    EXPECT_EQ(util::any_cast<int>(a2), 42);
-    EXPECT_EQ(util::any_cast<double>(a1), 3.14);
-
-    // test that underlying pointers did not change
-    EXPECT_EQ(pi, util::any_cast<int>(&a2));
-    EXPECT_EQ(pd, util::any_cast<double>(&a1));
-}
-
-// test operator=(const any&)
-TEST(any, assignment_from_lvalue) {
-    using std::string;
-
-    auto str1 = string("one");
-    auto str2 = string("two");
-    util::any a(str1);
-
-    util::any b;
-    b = a; // copy assignment
-
-    // verify that b contains value stored in a
-    EXPECT_EQ(str1, util::any_cast<string>(b));
-
-    // change the value stored in b
-    *util::any_cast<string>(&b) = str2;
-
-    // verify that a is unchanged and that b holds new value
-    EXPECT_EQ(str1, util::any_cast<string>(a));
-    EXPECT_EQ(str2, util::any_cast<string>(b));
-}
-
-// test operator=(any&&)
-TEST(any, assignment_from_rvalue) {
-    using std::string;
-
-    auto str1 = string("one");
-    auto str2 = string("two");
-    util::any a(str1);
-
-    util::any b;
-    b = std::move(a); // move assignment
-
-    EXPECT_EQ(str1, util::any_cast<string>(b));
-
-    EXPECT_EQ(nullptr, util::any_cast<string>(&a));
-}
-
-// test template<typename T> operator=(T&&)
-TEST(any, assignment_from_value) {
-    std::vector<int> tmp{1, 2, 3};
-
-    // take a pointer to the orignal data to later verify
-    // that the value was moved, and not copied.
-    auto ptr = tmp.data();
-
-    util::any a;
-    a = std::move(tmp);
-
-    auto vec = util::any_cast<std::vector<int>>(&a);
-
-    // ensure the value was moved
-    EXPECT_EQ(ptr, vec->data());
-
-    // ensure that the contents of the vector are unchanged
-    std::vector<int> ref{1, 2, 3};
-    EXPECT_EQ(ref, *vec);
-}
-
-TEST(any, make_any) {
-    using util::make_any;
-    using util::any_cast;
-
-    {
-        auto a = make_any<int>(42);
-
-        EXPECT_EQ(typeid(int), a.type());
-        EXPECT_EQ(42, any_cast<int>(a));
-    }
-
-    // check casting
-    {
-        auto a = make_any<double>(42u);
-
-        EXPECT_EQ(typeid(double), a.type());
-        EXPECT_EQ(42.0, any_cast<double>(a));
-    }
-
-    // check forwarding of parameters to constructor
-    {
-        // create a string from const char*
-        auto a = make_any<std::string>("hello");
-
-        EXPECT_EQ(any_cast<std::string>(a), "hello"s);
-    }
-
-    // test that we make_any correctly forwards rvalue arguments to the constructor
-    // of the contained object.
-    {
-        std::vector<int> tmp{1, 2, 3};
-
-        // take a pointer to the orignal data to later verify
-        // that the value was moved, and not copied.
-        auto ptr = tmp.data();
-
-        auto a = make_any<std::vector<int>>(std::move(tmp));
-
-        auto vec = any_cast<std::vector<int>>(&a);
-
-        // ensure the value was moved
-        EXPECT_EQ(ptr, vec->data());
-
-        // ensure that the contents of the vector are unchanged
-        std::vector<int> ref{1, 2, 3};
-        EXPECT_EQ(ref, *vec);
-    }
-}
-
-// Tests below are for `any_ptr` class.
-
-TEST(any_ptr, ctor_and_assign) {
-    using util::any_ptr;
-
-    any_ptr p;
-
-    EXPECT_FALSE(p);
-    EXPECT_FALSE(p.has_value());
-
-    int x;
-    any_ptr q(&x);
-
-    EXPECT_TRUE(q);
-    EXPECT_TRUE(q.has_value());
-
-    p = q;
-
-    EXPECT_TRUE(p);
-    EXPECT_TRUE(p.has_value());
-
-    p = nullptr;
-
-    EXPECT_FALSE(p);
-    EXPECT_FALSE(p.has_value());
-
-    p = &x;
-
-    EXPECT_TRUE(p);
-    EXPECT_TRUE(p.has_value());
-
-    p.reset();
-
-    EXPECT_FALSE(p);
-    EXPECT_FALSE(p.has_value());
-
-    p.reset(&x);
-
-    EXPECT_TRUE(p);
-    EXPECT_TRUE(p.has_value());
-
-    p.reset(nullptr);
-
-    EXPECT_FALSE(p);
-    EXPECT_FALSE(p.has_value());
-
-    p = nullptr;
-
-    EXPECT_FALSE(p);
-    EXPECT_FALSE(p.has_value());
-}
-
-
-TEST(any_ptr, ordering) {
-    using util::any_ptr;
-
-    int x[2];
-    double y;
-
-    any_ptr a(&x[0]);
-    any_ptr b(&x[1]);
-
-    EXPECT_LT(a, b);
-    EXPECT_LE(a, b);
-    EXPECT_NE(a, b);
-    EXPECT_GE(b, a);
-    EXPECT_GT(b, a);
-    EXPECT_FALSE(a==b);
-
-    any_ptr c(&y);
-
-    EXPECT_NE(c, a);
-    EXPECT_TRUE(a<c || a>c);
-    EXPECT_FALSE(a==c);
-}
-
-TEST(any_ptr, as_and_type) {
-    using util::any_ptr;
-
-    int x = 0;
-    const int y = 0;
-    any_ptr p;
-
-    EXPECT_FALSE(p.as<int*>());
-
-    p = &y;
-    EXPECT_FALSE(p.as<int*>());
-    EXPECT_TRUE(p.as<const int*>());
-    EXPECT_EQ(typeid(const int*), p.type());
-
-    p = &x;
-    EXPECT_TRUE(p.as<int*>());
-    EXPECT_FALSE(p.as<const int*>());
-    EXPECT_EQ(typeid(int*), p.type());
-
-    *p.as<int*>() = 3;
-    EXPECT_EQ(3, x);
-}
-
-TEST(any_ptr, any_cast) {
-    using util::any_ptr;
-    using util::any_cast;
-
-    int x = 0;
-    any_ptr p;
-
-    auto c1 = any_cast<int*>(p);
-    EXPECT_FALSE(c1);
-    EXPECT_TRUE((std::is_same<int*, decltype(c1)>::value));
-
-    p = &x;
-    auto c2 = any_cast<int*>(p);
-    EXPECT_TRUE(c2);
-}
-
diff --git a/test/unit/test_any_cast.cpp b/test/unit/test_any_cast.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0d6bbc233a01a49f4c854679e6acf0c046d9bcb3
--- /dev/null
+++ b/test/unit/test_any_cast.cpp
@@ -0,0 +1,52 @@
+#include <any>
+#include <type_traits>
+
+#include <arbor/util/any_cast.hpp>
+
+#include "../gtest.h"
+
+using namespace arb;
+
+// Check std::any_cast follows std::any_cast semantics
+// when applied to std::any objects.
+
+#define ASSERT_ANY_CAST_SAME_TYPE(type, value)\
+ASSERT_TRUE((std::is_same_v<decltype(std::any_cast<type>(value)), decltype(util::any_cast<type>(value))>))
+
+TEST(any_cast, pointer) {
+    std::any ai(42);
+
+    ASSERT_ANY_CAST_SAME_TYPE(int, &ai);
+    ASSERT_TRUE(util::any_cast<int>(&ai));
+    EXPECT_EQ(42, *util::any_cast<int>(&ai));
+
+    ASSERT_ANY_CAST_SAME_TYPE(const int, &ai);
+    ASSERT_TRUE(util::any_cast<const int>(&ai));
+    EXPECT_EQ(42, *util::any_cast<const int>(&ai));
+
+    const auto& cai = ai;
+    ASSERT_ANY_CAST_SAME_TYPE(int, &cai);
+    ASSERT_TRUE(util::any_cast<int>(&cai));
+    EXPECT_EQ(42, *util::any_cast<int>(&cai));
+
+    // Assert type mismatch or empty any gives nullptr.
+
+    std::any empty;
+    EXPECT_EQ(util::any_cast<int>(&empty), nullptr);
+    EXPECT_EQ(util::any_cast<float>(&ai), nullptr);
+    EXPECT_EQ(util::any_cast<int>((std::any*)nullptr), nullptr);
+}
+
+TEST(any_cast, ref_and_value) {
+    std::any ai(42);
+
+    ASSERT_ANY_CAST_SAME_TYPE(int, ai);
+    ASSERT_ANY_CAST_SAME_TYPE(int&, ai);
+    ASSERT_ANY_CAST_SAME_TYPE(const int&, ai);
+    ASSERT_ANY_CAST_SAME_TYPE(int&&, std::move(ai));
+
+    EXPECT_EQ(42, util::any_cast<int>(ai));
+    EXPECT_EQ(42, util::any_cast<int&>(ai));
+    EXPECT_EQ(42, util::any_cast<const int&>(ai));
+    EXPECT_EQ(42, util::any_cast<int&&>(std::move(ai)));
+}
diff --git a/test/unit/test_any_ptr.cpp b/test/unit/test_any_ptr.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7f4fcf4498a1c8788e93b2f6bffcfb4d55015f32
--- /dev/null
+++ b/test/unit/test_any_ptr.cpp
@@ -0,0 +1,129 @@
+#include <any>
+#include <type_traits>
+#include <typeinfo>
+
+#include <arbor/util/any_cast.hpp>
+#include <arbor/util/any_ptr.hpp>
+
+#include "../gtest.h"
+
+using namespace arb;
+using util::any_cast;
+using util::any_ptr;
+
+TEST(any_ptr, ctor_and_assign) {
+    using util::any_ptr;
+
+    any_ptr p;
+
+    EXPECT_FALSE(p);
+    EXPECT_FALSE(p.has_value());
+
+    int x;
+    any_ptr q(&x);
+
+    EXPECT_TRUE(q);
+    EXPECT_TRUE(q.has_value());
+
+    p = q;
+
+    EXPECT_TRUE(p);
+    EXPECT_TRUE(p.has_value());
+
+    p = nullptr;
+
+    EXPECT_FALSE(p);
+    EXPECT_FALSE(p.has_value());
+
+    p = &x;
+
+    EXPECT_TRUE(p);
+    EXPECT_TRUE(p.has_value());
+
+    p.reset();
+
+    EXPECT_FALSE(p);
+    EXPECT_FALSE(p.has_value());
+
+    p.reset(&x);
+
+    EXPECT_TRUE(p);
+    EXPECT_TRUE(p.has_value());
+
+    p.reset(nullptr);
+
+    EXPECT_FALSE(p);
+    EXPECT_FALSE(p.has_value());
+
+    p = nullptr;
+
+    EXPECT_FALSE(p);
+    EXPECT_FALSE(p.has_value());
+}
+
+TEST(any_ptr, ordering) {
+    int x[2];
+    double y;
+
+    any_ptr a(&x[0]);
+    any_ptr b(&x[1]);
+
+    EXPECT_LT(a, b);
+    EXPECT_LE(a, b);
+    EXPECT_NE(a, b);
+    EXPECT_GE(b, a);
+    EXPECT_GT(b, a);
+    EXPECT_FALSE(a==b);
+
+    any_ptr c(&y);
+
+    EXPECT_NE(c, a);
+    EXPECT_TRUE(a<c || a>c);
+    EXPECT_FALSE(a==c);
+}
+
+TEST(any_ptr, as_and_type) {
+    int x = 0;
+    const int y = 0;
+    any_ptr p;
+
+    EXPECT_FALSE(p.as<int*>());
+
+    p = &y;
+    EXPECT_FALSE(p.as<int*>());
+    EXPECT_TRUE(p.as<const int*>());
+    EXPECT_EQ(typeid(const int*), p.type());
+
+    p = &x;
+    EXPECT_TRUE(p.as<int*>());
+    EXPECT_FALSE(p.as<const int*>());
+    EXPECT_EQ(typeid(int*), p.type());
+
+    *p.as<int*>() = 3;
+    EXPECT_EQ(3, x);
+}
+
+TEST(any_ptr, any_cast) {
+    int x = 0;
+    any_ptr p;
+
+    auto c1 = any_cast<int*>(p);
+    EXPECT_FALSE(c1);
+    EXPECT_TRUE((std::is_same_v<int*, decltype(c1)>));
+
+    p = &x;
+    auto c2 = any_cast<int*>(p);
+    EXPECT_TRUE(c2);
+
+    auto c3 = any_cast<double*>(p);
+    EXPECT_FALSE(c3);
+
+    // Might want to reconsider these semantics, but here we are:
+    auto c4 = any_cast<const int*>(p);
+    EXPECT_FALSE(c4);
+
+    p = (const int*)&x;
+    auto c5 = any_cast<int*>(p);
+    EXPECT_FALSE(c5);
+}
+
diff --git a/test/unit/test_any_visitor.cpp b/test/unit/test_any_visitor.cpp
index 6c22ec07b64fb930fd2cd0c8aa1e9b381d162833..242e5257417cae7fb06a1e412d64353c053256d3 100644
--- a/test/unit/test_any_visitor.cpp
+++ b/test/unit/test_any_visitor.cpp
@@ -1,19 +1,20 @@
+#include <any>
 #include <string>
 #include <type_traits>
 #include <typeinfo>
 
-#include <arbor/util/any.hpp>
 #include <arbor/util/any_visitor.hpp>
 
 #include "../gtest.h"
 #include "common.hpp"
 
 using namespace std::string_literals;
-using arb::util::any;
-using arb::util::any_cast;
-using arb::util::any_visitor;
-using arb::util::bad_any_cast;
+using std::any;
+using std::any_cast;
+using std::bad_any_cast;
+
 using arb::util::overload;
+using arb::util::any_visitor;
 
 TEST(any_visitor, simple) {
     enum { A0, B0, C0 };
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index 5075872077fa91ff7ddbb80cf3571edae9b869f2..12e2b912f793051bb7d69626a97afe5d879be8c9 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -13,6 +13,7 @@
 #include <arbor/sampling.hpp>
 #include <arbor/simulation.hpp>
 #include <arbor/schedule.hpp>
+#include <arbor/util/any_ptr.hpp>
 
 #include <arborenv/concurrency.hpp>
 
@@ -240,12 +241,12 @@ TEST(fvm_lowered, matrix_init)
     auto n = J.size();
     auto& mat = J.state_;
 
-    EXPECT_FALSE(util::any_of(util::subrange_view(mat.u, 1, n), isnan));
-    EXPECT_FALSE(util::any_of(mat.d, isnan));
-    EXPECT_FALSE(util::any_of(S->voltage, isnan));
+    EXPECT_FALSE(arb::util::any_of(util::subrange_view(mat.u, 1, n), isnan));
+    EXPECT_FALSE(arb::util::any_of(mat.d, isnan));
+    EXPECT_FALSE(arb::util::any_of(S->voltage, isnan));
 
-    EXPECT_FALSE(util::any_of(util::subrange_view(mat.u, 1, n), ispos));
-    EXPECT_FALSE(util::any_of(mat.d, isneg));
+    EXPECT_FALSE(arb::util::any_of(util::subrange_view(mat.u, 1, n), ispos));
+    EXPECT_FALSE(arb::util::any_of(mat.d, isneg));
 }
 
 TEST(fvm_lowered, target_handles) {
diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp
index 7b363f2adbb6c81be5aa6fd6d1847d1b74da8723..3c11796542bd78adfce5d81610bc4c9dfa327845 100644
--- a/test/unit/test_probe.cpp
+++ b/test/unit/test_probe.cpp
@@ -13,6 +13,7 @@
 #include <arbor/schedule.hpp>
 #include <arbor/simple_sampler.hpp>
 #include <arbor/simulation.hpp>
+#include <arbor/util/any_cast.hpp>
 #include <arbor/util/any_ptr.hpp>
 #include <arbor/util/pp_util.hpp>
 #include <arbor/version.hpp>
@@ -37,6 +38,7 @@
 #include "../simple_recipes.hpp"
 
 using namespace arb;
+using util::any_cast;
 
 using multicore_fvm_cell = fvm_lowered_cell_impl<multicore::backend>;
 using multicore_shared_state = multicore::backend::shared_state;
@@ -142,9 +144,9 @@ void run_v_i_probe_test(const context& ctx) {
     // to wrap an fvm_probe_interpolated; ion current density is
     // a scalar, so should wrap fvm_probe_scalar.
 
-    ASSERT_TRUE(util::get_if<fvm_probe_interpolated>(probe_map.data_on({0, 0}).front().info));
-    ASSERT_TRUE(util::get_if<fvm_probe_interpolated>(probe_map.data_on({0, 0}).front().info));
-    ASSERT_TRUE(util::get_if<fvm_probe_scalar>(probe_map.data_on({0, 2}).front().info));
+    ASSERT_TRUE(std::get_if<fvm_probe_interpolated>(&probe_map.data_on({0, 0}).front().info));
+    ASSERT_TRUE(std::get_if<fvm_probe_interpolated>(&probe_map.data_on({0, 0}).front().info));
+    ASSERT_TRUE(std::get_if<fvm_probe_scalar>(&probe_map.data_on({0, 2}).front().info));
 
     probe_handle p0a = get_probe_raw_handle({0, 0}, 0);
     probe_handle p0b = get_probe_raw_handle({0, 0}, 1);
@@ -225,11 +227,11 @@ void run_v_cell_probe_test(const context& ctx) {
 
         ASSERT_EQ(1u, probe_map.size());
 
-        const fvm_probe_multi* h_ptr = util::get_if<fvm_probe_multi>(probe_map.data_on({0, 0}).front().info);
+        const fvm_probe_multi* h_ptr = std::get_if<fvm_probe_multi>(&probe_map.data_on({0, 0}).front().info);
         ASSERT_TRUE(h_ptr);
         auto& h = *h_ptr;
 
-        const mcable_list* cl_ptr = util::get_if<mcable_list>(h_ptr->metadata);
+        const mcable_list* cl_ptr = std::get_if<mcable_list>(&h_ptr->metadata);
         ASSERT_TRUE(cl_ptr);
         auto& cl = *cl_ptr;
 
@@ -420,10 +422,10 @@ void run_expsyn_g_cell_probe_test(const context& ctx) {
 
         ASSERT_EQ(2u, probe_map.size());
         for (unsigned i: {0u, 1u}) {
-            const auto* h_ptr = util::get_if<fvm_probe_multi>(probe_map.data_on({i, 0}).front().info);
+            const auto* h_ptr = std::get_if<fvm_probe_multi>(&probe_map.data_on({i, 0}).front().info);
             ASSERT_TRUE(h_ptr);
 
-            const auto* m_ptr = util::get_if<std::vector<cable_probe_point_info>>(h_ptr->metadata);
+            const auto* m_ptr = std::get_if<std::vector<cable_probe_point_info>>(&h_ptr->metadata);
             ASSERT_TRUE(m_ptr);
 
             const fvm_probe_multi& h = *h_ptr;
@@ -609,11 +611,11 @@ void run_ion_density_probe_test(const context& ctx) {
     // sorted by CV in the fvm_probe_weighted_multi object; this is assumed
     // below.
 
-    auto* p_ptr = util::get_if<fvm_probe_multi>(probe_map.data_on({0, 11}).front().info);
+    auto* p_ptr = std::get_if<fvm_probe_multi>(&probe_map.data_on({0, 11}).front().info);
     ASSERT_TRUE(p_ptr);
     const fvm_probe_multi& na_int_all_info = *p_ptr;
 
-    auto* m_ptr = util::get_if<mcable_list>(na_int_all_info.metadata);
+    auto* m_ptr = std::get_if<mcable_list>(&na_int_all_info.metadata);
     ASSERT_TRUE(m_ptr);
     mcable_list na_int_all_metadata = *m_ptr;
 
@@ -627,11 +629,11 @@ void run_ion_density_probe_test(const context& ctx) {
     EXPECT_EQ(na_int_cv2,   na_int_all_info.raw_handles[1]);
     EXPECT_EQ(na_int_cv2-1, na_int_all_info.raw_handles[0]);
 
-    p_ptr = util::get_if<fvm_probe_multi>(probe_map.data_on({0, 12}).front().info);
+    p_ptr = std::get_if<fvm_probe_multi>(&probe_map.data_on({0, 12}).front().info);
     ASSERT_TRUE(p_ptr);
     const fvm_probe_multi& ca_ext_all_info = *p_ptr;
 
-    m_ptr = util::get_if<mcable_list>(ca_ext_all_info.metadata);
+    m_ptr = std::get_if<mcable_list>(&ca_ext_all_info.metadata);
     ASSERT_TRUE(m_ptr);
     mcable_list ca_ext_all_metadata = *m_ptr;
 
@@ -830,12 +832,12 @@ void run_axial_and_ion_current_sampled_probe_test(const context& ctx) {
             ASSERT_EQ(1u, n_sample);
 
             if (pm.tag==1) { // (whole cell probe)
-                const mcable_list* m = util::any_cast<const mcable_list*>(pm.meta);
+                const mcable_list* m = any_cast<const mcable_list*>(pm.meta);
                 ASSERT_NE(nullptr, m);
                 // Metadata should comprise one cable per CV.
                 ASSERT_EQ(n_cv, m->size());
 
-                const cable_sample_range* s = util::any_cast<const cable_sample_range*>(samples[0].data);
+                const cable_sample_range* s = any_cast<const cable_sample_range*>(samples[0].data);
                 ASSERT_NE(nullptr, s);
                 ASSERT_EQ(s->first+n_cv, s->second);
 
@@ -847,10 +849,10 @@ void run_axial_and_ion_current_sampled_probe_test(const context& ctx) {
                 // Probe id tells us which axial current this is.
                 ASSERT_LT(pm.id.index, n_axial_probe);
 
-                const mlocation* m = util::any_cast<const mlocation*>(pm.meta);
+                const mlocation* m = any_cast<const mlocation*>(pm.meta);
                 ASSERT_NE(nullptr, m);
 
-                const double* s = util::any_cast<const double*>(samples[0].data);
+                const double* s = any_cast<const double*>(samples[0].data);
                 ASSERT_NE(nullptr, s);
 
                 i_axial.at(pm.id.index) = *s;
@@ -889,7 +891,7 @@ auto run_simple_samplers(
     double t_end,
     const std::vector<cable_cell>& cells,
     cell_gid_type probe_cell,
-    const std::vector<util::any>& probe_addrs,
+    const std::vector<std::any>& probe_addrs,
     const std::vector<double>& when)
 {
     cable1d_recipe rec(cells, false);
@@ -920,7 +922,7 @@ auto run_simple_sampler(
     double t_end,
     const std::vector<cable_cell>& cells,
     cell_gid_type probe_cell,
-    const util::any& probe_addr,
+    const std::any& probe_addr,
     const std::vector<double>& when)
 {
     return run_simple_samplers<SampleData, SampleMeta>(ctx, t_end, cells, probe_cell, {probe_addr}, when).at(0);
@@ -1187,8 +1189,8 @@ void run_exact_sampling_probe_test(const context& ctx) {
             return {explicit_generator(spikes)};
         }
 
-        util::any get_global_properties(cell_kind k) const override {
-            return k==cell_kind::cable? gprop_: util::any{};
+        std::any get_global_properties(cell_kind k) const override {
+            return k==cell_kind::cable? gprop_: std::any{};
         }
     };
 
@@ -1316,9 +1318,9 @@ TEST(probe, get_probe_metadata) {
     EXPECT_EQ(7, mm[1].tag);
     EXPECT_EQ(7, mm[2].tag);
 
-    const mlocation* l0 = util::any_cast<const mlocation*>(mm[0].meta);
-    const mlocation* l1 = util::any_cast<const mlocation*>(mm[1].meta);
-    const mlocation* l2 = util::any_cast<const mlocation*>(mm[2].meta);
+    const mlocation* l0 = any_cast<const mlocation*>(mm[0].meta);
+    const mlocation* l1 = any_cast<const mlocation*>(mm[1].meta);
+    const mlocation* l2 = any_cast<const mlocation*>(mm[2].meta);
 
     ASSERT_TRUE(l0);
     ASSERT_TRUE(l1);
diff --git a/test/unit/test_range.cpp b/test/unit/test_range.cpp
index 9b6a14f1c123bf78a17b4316d2b5067d3ace8159..846811d03d151225a7f9df2d6c0c8297e7d033f8 100644
--- a/test/unit/test_range.cpp
+++ b/test/unit/test_range.cpp
@@ -65,7 +65,7 @@ TEST(range, pointer) {
     util::range<int *> s(&xs[l], &xs[r]);
     auto s_deduced = util::make_range(xs+l, xs+r);
 
-    EXPECT_TRUE((std::is_same<decltype(s), decltype(s_deduced)>::value));
+    EXPECT_TRUE((std::is_same_v<decltype(s), decltype(s_deduced)>));
     EXPECT_EQ(s.left, s_deduced.left);
     EXPECT_EQ(s.right, s_deduced.right);
 
@@ -142,11 +142,11 @@ TEST(range, input_iterator) {
 TEST(range, const_iterator) {
     std::vector<int> xs = { 1, 2, 3, 4, 5 };
     auto r = util::make_range(xs.begin(), xs.end());
-    EXPECT_TRUE((std::is_same<int&, decltype(r.front())>::value));
+    EXPECT_TRUE((std::is_same_v<int&, decltype(r.front())>));
 
     const auto& xs_const = xs;
     auto r_const = util::make_range(xs_const.begin(), xs_const.end());
-    EXPECT_TRUE((std::is_same<const int&, decltype(r_const.front())>::value));
+    EXPECT_TRUE((std::is_same_v<const int&, decltype(r_const.front())>));
 }
 
 TEST(range, view) {
@@ -188,7 +188,7 @@ TEST(range, strictify) {
     auto cstr_range = util::make_range(cstr, null_terminated);
 
     auto ptr_range = util::strict_view(cstr_range);
-    EXPECT_TRUE((std::is_same<decltype(ptr_range), util::range<const char *>>::value));
+    EXPECT_TRUE((std::is_same_v<decltype(ptr_range), util::range<const char *>>));
     EXPECT_EQ(cstr, ptr_range.left);
     EXPECT_EQ(cstr+11, ptr_range.right);
 
@@ -259,7 +259,7 @@ TEST(range, max_element_by) {
     auto i = util::max_element_by(cstr_range,
         [](char c) -> int { return -c; });
 
-    EXPECT_TRUE((std::is_same<const char *, decltype(i)>::value));
+    EXPECT_TRUE((std::is_same_v<const char *, decltype(i)>));
     EXPECT_EQ('d', *i);
     EXPECT_EQ(cstr+9, i);
 
diff --git a/test/unit/test_unique_any.cpp b/test/unit/test_unique_any.cpp
index 02612a0900c1be1d84cc8fbf824a257caf4f8c28..31e99d5d18e0883a3b74f20ff758e74cbabf7497 100644
--- a/test/unit/test_unique_any.cpp
+++ b/test/unit/test_unique_any.cpp
@@ -1,5 +1,6 @@
 #include <iostream>
 
+#include <arbor/util/any_cast.hpp>
 #include <arbor/util/unique_any.hpp>
 
 #include "util/rangeutil.hpp"
@@ -9,6 +10,7 @@
 #include "common.hpp"
 
 using namespace arb;
+using util::any_cast;
 
 TEST(unique_any, copy_construction) {
     using util::unique_any;
@@ -48,11 +50,11 @@ TEST(unique_any, move_construction) {
     util::unique_any moved(std::move(m));
 
     // Check that the expected number of copies and moves were performed.
-    const auto& cref = util::any_cast<const moveable&>(copied);
+    const auto& cref = any_cast<const moveable&>(copied);
     EXPECT_EQ(cref.moves, 0);
     EXPECT_EQ(cref.copies, 1);
 
-    const auto& mref = util::any_cast<const moveable&>(moved);
+    const auto& mref = any_cast<const moveable&>(moved);
     EXPECT_EQ(mref.moves, 1);
     EXPECT_EQ(mref.copies, 0);
 
@@ -60,11 +62,11 @@ TEST(unique_any, move_construction) {
     // constructed value
     util::unique_any fin(std::move(moved));
     EXPECT_FALSE(moved.has_value()); // moved has been moved from and should be empty
-    const auto& fref = util::any_cast<const moveable&>(fin);
+    const auto& fref = any_cast<const moveable&>(fin);
     EXPECT_EQ(fref.moves, 1);
     EXPECT_EQ(fref.copies, 0);
 
-    const auto value = util::any_cast<moveable>(fin);
+    const auto value = any_cast<moveable>(fin);
     EXPECT_EQ(value.moves, 1);
     EXPECT_EQ(value.copies, 1);
 }
@@ -91,7 +93,6 @@ TEST(unique_any, type) {
 
 TEST(unique_any, swap) {
     using util::unique_any;
-    using util::any_cast;
 
     unique_any any1(42);     // integer
     unique_any any2(3.14);   // double
@@ -121,24 +122,24 @@ TEST(unique_any, not_copy_constructable) {
 
     util::unique_any a(T(42));
 
-    auto& ref = util::any_cast<T&>(a);
+    auto& ref = any_cast<T&>(a);
     EXPECT_EQ(ref, 42);
     ref.value = 100;
 
-    EXPECT_EQ(util::any_cast<T&>(a).value, 100);
+    EXPECT_EQ(any_cast<T&>(a).value, 100);
 
     // the following will fail if uncommented, because we are requesting
     // a copy of an non-copyable type.
 
-    //auto value = util::any_cast<T>(a);
+    //auto value = any_cast<T>(a);
 
     // Test that we can move the contents of the unique_any.
     // NOTE: it makes sense to sink a with std::move(a) instead
     // of the following, because after such an assignment a will
     // be in a moved from state, and enforcing std::move(a) it
     // is made clearer in the calling code that a has been invalidated.
-    //     util::any_cast<T&&>(a)
-    T val(util::any_cast<T&&>(std::move(a)));
+    //     any_cast<T&&>(a)
+    T val(any_cast<T&&>(std::move(a)));
     EXPECT_EQ(val.value, 100);
 }
 
@@ -151,32 +152,32 @@ TEST(unique_any, any_cast_ptr) {
     // test that valid pointers are returned for int and std::string types
 
     unique_any ai(42);
-    auto ptr_i = util::any_cast<int>(&ai);
+    auto ptr_i = any_cast<int>(&ai);
     EXPECT_EQ(*ptr_i, 42);
 
     unique_any as(std::string("hello"));
-    auto ptr_s = util::any_cast<std::string>(&as);
+    auto ptr_s = any_cast<std::string>(&as);
     EXPECT_EQ(*ptr_s, "hello");
 
     // test that exceptions are thrown for invalid casts
-    EXPECT_EQ(util::any_cast<int>(&as), nullptr);
-    EXPECT_EQ(util::any_cast<std::string>(&ai), nullptr);
+    EXPECT_EQ(any_cast<int>(&as), nullptr);
+    EXPECT_EQ(any_cast<std::string>(&ai), nullptr);
     unique_any empty;
-    EXPECT_EQ(util::any_cast<int>(&empty), nullptr);
-    EXPECT_EQ(util::any_cast<int>((util::unique_any*)nullptr), nullptr);
+    EXPECT_EQ(any_cast<int>(&empty), nullptr);
+    EXPECT_EQ(any_cast<int>((util::unique_any*)nullptr), nullptr);
 
     // Check that constness of the returned pointer matches that the input.
     // Check that constness of the returned pointer matches that the input.
     {
         unique_any a(42);
-        auto p = util::any_cast<int>(&a);
+        auto p = any_cast<int>(&a);
 
         // any_cast(any*) should not return const*
         EXPECT_TRUE((std::is_same<int*, decltype(p)>::value));
     }
     {
         const unique_any a(42);
-        auto p = util::any_cast<int>(&a);
+        auto p = any_cast<int>(&a);
 
         // any_cast(const any*) should return const*
         EXPECT_TRUE((std::is_same<const int*, decltype(p)>::value));
@@ -186,20 +187,20 @@ TEST(unique_any, any_cast_ptr) {
 // test any_cast(unique_any&)
 TEST(unique_any, any_cast_ref) {
     util::unique_any ai(42);
-    auto& i = util::any_cast<int&>(ai);
+    auto& i = any_cast<int&>(ai);
 
     EXPECT_EQ(typeid(i), typeid(int));
     EXPECT_EQ(i, 42);
 
     // any_cast<T>(unique_any&) returns a 
     i = 100;
-    EXPECT_EQ(util::any_cast<int>(ai), 100);
+    EXPECT_EQ(any_cast<int>(ai), 100);
 }
 
 // test any_cast(const unique_any&)
 TEST(unique_any, any_cast_const_ref) {
     const util::unique_any ai(42);
-    auto& i = util::any_cast<const int&>(ai);
+    auto& i = any_cast<const int&>(ai);
 
     EXPECT_EQ(typeid(i), typeid(int));
     EXPECT_EQ(i, 42);
@@ -209,7 +210,7 @@ TEST(unique_any, any_cast_const_ref) {
 
 // test any_cast(unique_any&&)
 TEST(unique_any, any_cast_rvalue) {
-    auto moved = util::any_cast<moveable>(util::unique_any(moveable()));
+    auto moved = any_cast<moveable>(util::unique_any(moveable()));
     EXPECT_EQ(moved.moves, 2);
     EXPECT_EQ(moved.copies, 0);
 }
@@ -218,18 +219,18 @@ TEST(unique_any, std_swap) {
     util::unique_any a1(42);
     util::unique_any a2(3.14);
 
-    auto pi = util::any_cast<int>(&a1);
-    auto pd = util::any_cast<double>(&a2);
+    auto pi = any_cast<int>(&a1);
+    auto pd = any_cast<double>(&a2);
 
     std::swap(a1, a2);
 
     // test that values were swapped
-    EXPECT_EQ(util::any_cast<int>(a2), 42);
-    EXPECT_EQ(util::any_cast<double>(a1), 3.14);
+    EXPECT_EQ(any_cast<int>(a2), 42);
+    EXPECT_EQ(any_cast<double>(a1), 3.14);
 
     // test that underlying pointers did not change
-    EXPECT_EQ(pi, util::any_cast<int>(&a2));
-    EXPECT_EQ(pd, util::any_cast<double>(&a1));
+    EXPECT_EQ(pi, any_cast<int>(&a2));
+    EXPECT_EQ(pd, any_cast<double>(&a1));
 }
 
 // test operator=(unique_any&&)
@@ -244,9 +245,9 @@ TEST(unique_any, assignment_from_rvalue) {
     unique_any b;
     b = std::move(a); // move assignment
 
-    EXPECT_EQ(str1, util::any_cast<string>(b));
+    EXPECT_EQ(str1, any_cast<string>(b));
 
-    EXPECT_EQ(nullptr, util::any_cast<string>(&a));
+    EXPECT_EQ(nullptr, any_cast<string>(&a));
 }
 
 // test template<typename T> operator=(T&&)
@@ -263,7 +264,7 @@ TEST(unique_any, assignment_from_value) {
         unique_any a;
         a = std::move(tmp);
 
-        auto vec = util::any_cast<std::vector<int>>(&a);
+        auto vec = any_cast<std::vector<int>>(&a);
 
         // ensure the value was moved
         EXPECT_EQ(ptr, vec->data());
@@ -299,7 +300,6 @@ TEST(unique_any, assignment_from_value) {
 
 TEST(unique_any, make_unique_any) {
     using util::make_unique_any;
-    using util::any_cast;
 
     {
         auto a = make_unique_any<int>(42);
@@ -366,7 +366,7 @@ TEST(unique_any, stdvector)
     // push_back
     {
         using T = testing::nocopy<std::string>;
-        auto get = [](const unique_any& v) {return util::any_cast<const T&>(v).value;};
+        auto get = [](const unique_any& v) {return any_cast<const T&>(v).value;};
 
         std::vector<unique_any> vec;
         vec.push_back(T("h"));
@@ -389,7 +389,7 @@ TEST(unique_any, stdvector)
 
     // sort
     {
-        auto get = [](const unique_any& v) {return util::any_cast<int>(v);};
+        auto get = [](const unique_any& v) {return any_cast<int>(v);};
         int n = 10;
         std::vector<unique_any> vec;
 
@@ -410,7 +410,7 @@ TEST(unique_any, stdvector)
     // std::reverse with non-copyable type
     {
         using T = testing::nocopy<int>;
-        auto get = [](const unique_any& v) {return util::any_cast<const T&>(v).value;};
+        auto get = [](const unique_any& v) {return any_cast<const T&>(v).value;};
         int n = 10;
         std::vector<unique_any> vec;
 
diff --git a/test/unit/test_variant.cpp b/test/unit/test_variant.cpp
deleted file mode 100644
index 8fe0e941553ed1b8e123a1ad022f52383a978cb3..0000000000000000000000000000000000000000
--- a/test/unit/test_variant.cpp
+++ /dev/null
@@ -1,537 +0,0 @@
-#include <tuple>
-
-#include <arbor/util/variant.hpp>
-#include "util/meta.hpp"
-
-#include "../gtest.h"
-#include "common.hpp"
-
-#ifdef __clang__
-#pragma clang diagnostic ignored "-Wself-assign-overloaded"
-#endif
-
-using namespace arb::util;
-using testing::nocopy;
-using testing::nomove;
-
-TEST(variant, in_place_index_ctor) {
-    // Equal variant alternatives okay?
-    {
-        variant<int> v0{in_place_index<0>(), 3};
-        ASSERT_EQ(0u, v0.index());
-    }
-    {
-        variant<int, int> v0{in_place_index<0>(), 3};
-        ASSERT_EQ(0u, v0.index());
-
-        variant<int, int> v1{in_place_index<1>(), 3};
-        ASSERT_EQ(1u, v1.index());
-    }
-    {
-        variant<int, int, int> v0{in_place_index<0>(), 3};
-        ASSERT_EQ(0u, v0.index());
-
-        variant<int, int, int> v1{in_place_index<1>(), 3};
-        ASSERT_EQ(1u, v1.index());
-
-        variant<int, int, int> v2{in_place_index<2>(), 3};
-        ASSERT_EQ(2u, v2.index());
-    }
-
-    // Check move- and copy- only types work.
-    {
-        struct foo { explicit foo(int, double) {} };
-        nocopy<foo>::reset_counts();
-        nomove<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v0(in_place_index<0>(), 1, 3.2);
-        ASSERT_EQ(0u, v0.index());
-        EXPECT_EQ(0, nocopy<foo>::move_ctor_count); // (should have constructed in-place)
-        EXPECT_EQ(0, nocopy<foo>::move_assign_count);
-        nocopy<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v0bis(in_place_index<0>(), nocopy<foo>(1, 3.2));
-        ASSERT_EQ(0u, v0.index());
-        EXPECT_EQ(1, nocopy<foo>::move_ctor_count); // (should have move-constructed)
-        EXPECT_EQ(0, nocopy<foo>::move_assign_count); // (should have constructed in-place)
-        nocopy<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v1(in_place_index<1>(), 1, 3.2);
-        ASSERT_EQ(1u, v1.index());
-        EXPECT_EQ(0, nomove<foo>::copy_ctor_count); // (should have constructed in-place)
-        EXPECT_EQ(0, nomove<foo>::copy_assign_count);
-        nomove<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v1bis(in_place_index<1>(), nomove<foo>(1, 3.2));
-        ASSERT_EQ(1u, v1bis.index());
-        EXPECT_EQ(1, nomove<foo>::copy_ctor_count); // (should have copy-constructed)
-        EXPECT_EQ(0, nomove<foo>::copy_assign_count);
-        nomove<foo>::reset_counts();
-    }
-}
-
-TEST(variant, in_place_type_ctor) {
-    {
-        variant<int> v0{in_place_type<int>(), 3};
-        ASSERT_EQ(0u, v0.index());
-    }
-    {
-        variant<int, double> v0{in_place_type<int>(), 3};
-        ASSERT_EQ(0u, v0.index());
-
-        variant<int, double> v1{in_place_type<double>(), 3};
-        ASSERT_EQ(1u, v1.index());
-    }
-    // Check move- and copy- only types for in_place_type too.
-    {
-        struct foo { explicit foo(int, double) {} };
-        nocopy<foo>::reset_counts();
-        nomove<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v0(in_place_type<nocopy<foo>>(), 1, 3.2);
-        ASSERT_EQ(0u, v0.index());
-        EXPECT_EQ(0, nocopy<foo>::move_ctor_count); // (should have constructed in-place)
-        EXPECT_EQ(0, nocopy<foo>::move_assign_count);
-        nocopy<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v0bis(in_place_type<nocopy<foo>>(), nocopy<foo>(1, 3.2));
-        ASSERT_EQ(0u, v0.index());
-        EXPECT_EQ(1, nocopy<foo>::move_ctor_count); // (should have move-constructed)
-        EXPECT_EQ(0, nocopy<foo>::move_assign_count); // (should have constructed in-place)
-        nocopy<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v1(in_place_type<nomove<foo>>(), 1, 3.2);
-        ASSERT_EQ(1u, v1.index());
-        EXPECT_EQ(0, nomove<foo>::copy_ctor_count); // (should have constructed in-place)
-        EXPECT_EQ(0, nomove<foo>::copy_assign_count);
-        nomove<foo>::reset_counts();
-
-        variant<nocopy<foo>, nomove<foo>> v1bis(in_place_type<nomove<foo>>(), nomove<foo>(1, 3.2));
-        ASSERT_EQ(1u, v1bis.index());
-        EXPECT_EQ(1, nomove<foo>::copy_ctor_count); // (should have copy-constructed)
-        EXPECT_EQ(0, nomove<foo>::copy_assign_count);
-        nomove<foo>::reset_counts();
-    }
-}
-
-TEST(variant, converting_ctor) {
-    struct Z {};
-    struct X { X() {} X(Z) {} };
-    struct Y {};
-
-    // Expect resolution via overload set of one-argument constructors.
-    {
-        using var_xy = variant<X, Y>;
-        var_xy v0(X{});
-        ASSERT_EQ(0u, v0.index());
-
-        var_xy v1(Y{});
-        ASSERT_EQ(1u, v1.index());
-
-        var_xy v0bis(Z{});
-        ASSERT_EQ(0u, v0bis.index());
-    }
-    {
-        using var_xyz = variant<X, Y, Z>;
-        var_xyz v0(X{});
-        ASSERT_EQ(0u, v0.index());
-
-        var_xyz v1(Y{});
-        ASSERT_EQ(1u, v1.index());
-
-        var_xyz v2(Z{});
-        ASSERT_EQ(2u, v2.index());
-    }
-
-    // A bool alternative should only accept (cvref qualified) bool.
-    {
-        using bool_or_ptr = variant<bool, void*>;
-        bool_or_ptr v0(false);
-        ASSERT_EQ(0u, v0.index());
-
-        bool_or_ptr v1(nullptr);
-        ASSERT_EQ(1u, v1.index());
-    }
-}
-
-TEST(variant, get) {
-    struct X {};
-
-    {
-        variant<int, double, X> v(2.3);
-
-        EXPECT_THROW(get<0>(v), bad_variant_access);
-        EXPECT_EQ(2.3, get<1>(v));
-
-        EXPECT_THROW(get<int>(v), bad_variant_access);
-        EXPECT_EQ(2.3, get<double>(v));
-    }
-    {
-        variant<nocopy<double>> v(3.1);
-        auto x = get<0>(std::move(v));
-        // nocopy will zero value on move
-        EXPECT_EQ(3.1, x.value);
-        EXPECT_EQ(0.0, get<0>(v).value);
-    }
-    {
-        // should be able to modify in-place
-        variant<double> v(3.1);
-        get<0>(v) = 4.2;
-        EXPECT_EQ(4.2, get<0>(v));
-    }
-}
-
-TEST(variant, get_if) {
-    struct X {};
-
-    {
-        variant<int, double, X> v(2.3);
-
-        EXPECT_EQ(nullptr, get_if<0>(v));
-        ASSERT_NE(nullptr, get_if<1>(v));
-        EXPECT_EQ(2.3, *get_if<1>(v));
-
-        EXPECT_EQ(nullptr, get_if<int>(v));
-        ASSERT_NE(nullptr, get_if<double>(v));
-        EXPECT_EQ(2.3, *get_if<double>(v));
-    }
-    {
-        // should be able to modify in-place
-        variant<double> v(3.1);
-        ASSERT_NE(nullptr, get_if<0>(v));
-        *get_if<0>(v) = 4.2;
-        EXPECT_EQ(4.2, get<0>(v));
-    }
-}
-
-TEST(variant, visit) {
-    struct X {};
-
-    // void case
-    struct visitor {
-        int* result = nullptr;
-        visitor(int& r): result(&r) {}
-
-        void operator()(int) { *result = 10; }
-        void operator()(double) { *result = 11; }
-        void operator()(X) { *result = 12; }
-    };
-
-    variant<int, double, X> v0(2);
-    variant<int, double, X> v1(3.1);
-    variant<int, double, X> v2(X{});
-
-    int r;
-    auto hello = visitor(r);
-
-    visit<void>(hello, v0);
-    EXPECT_EQ(10, r);
-
-    visit<void>(hello, v1);
-    EXPECT_EQ(11, r);
-
-    visit<void>(hello, v2);
-    EXPECT_EQ(12, r);
-}
-
-TEST(variant, visit_deduce_return) {
-    struct X {};
-
-    struct visitor {
-        char operator()(int) { return 'i'; }
-        char operator()(double) { return 'd'; }
-        char operator()(X) { return 'X'; }
-    } hello;
-
-    using variant_idX = variant<int, double, X>;
-
-    EXPECT_EQ('i', visit(hello, variant_idX(1)));
-    EXPECT_EQ('d', visit(hello, variant_idX(1.1)));
-    EXPECT_EQ('X', visit(hello, variant_idX(X{})));
-}
-
-TEST(variant, valueless) {
-    struct X {
-        X() {}
-        X(const X&) { throw "nope"; }
-    };
-
-    variant<X, int> vx;
-    variant<X, int> vi(3);
-
-    ASSERT_EQ(0u, vx.index());
-    ASSERT_EQ(1u, vi.index());
-    try {
-        vi = vx;
-    }
-    catch (...) {
-    }
-    EXPECT_TRUE(vi.valueless_by_exception());
-    EXPECT_EQ(std::size_t(-1), vi.index());
-}
-
-namespace {
-struct nope {};
-
-struct maybe_throws_on_assign {
-    int i;
-    explicit maybe_throws_on_assign(int i): i(i) {}
-    maybe_throws_on_assign(const maybe_throws_on_assign& x): i(x.i) {}
-    maybe_throws_on_assign(maybe_throws_on_assign&& x): i(x.i) {}
-
-    maybe_throws_on_assign& operator=(const maybe_throws_on_assign& x) {
-        if (x.i<0) throw nope{};
-        i = x.i;
-        return *this;
-    }
-
-    maybe_throws_on_assign& operator=(maybe_throws_on_assign&& x) {
-        if (x.i<0) throw nope{};
-        i = x.i;
-        return *this;
-    }
-
-    ~maybe_throws_on_assign() { ++dtor; }
-
-    static int dtor;
-};
-
-int maybe_throws_on_assign::dtor = 0;
-} // anonymous namespace
-
-TEST(variant, copy_assign) {
-    struct X {
-        X& operator=(const X&) { throw nope{}; }
-    };
-
-    using vidX = variant<int, double, X>;
-
-    vidX v0{in_place_type<int>(), 3};
-    vidX v1{in_place_type<double>(), 4.};
-
-    vidX valueless{X{}};
-    try { valueless = valueless; } catch (...) {}
-    ASSERT_TRUE(valueless.valueless_by_exception());
-
-    vidX v;
-    v = v0;
-    ASSERT_EQ(0u, v.index());
-    EXPECT_EQ(3, get<0>(v));
-
-    v = v1;
-    ASSERT_EQ(1u, v.index());
-    EXPECT_EQ(4., get<1>(v));
-
-    v = valueless;
-    EXPECT_TRUE(v.valueless_by_exception());
-
-    v = v1;
-    ASSERT_EQ(1u, v.index());
-    EXPECT_EQ(4., get<1>(v));
-
-    using vbM = variant<bool, maybe_throws_on_assign>;
-    maybe_throws_on_assign::dtor = 0;
-
-    vbM w0(in_place_type<maybe_throws_on_assign>(), 2);
-    vbM w1(in_place_type<maybe_throws_on_assign>(), -2);
-    ASSERT_EQ(0, maybe_throws_on_assign::dtor);
-
-    EXPECT_THROW(w0 = w1, nope);
-    EXPECT_TRUE(w0.valueless_by_exception());
-    EXPECT_EQ(1, maybe_throws_on_assign::dtor);
-
-    maybe_throws_on_assign::dtor = 0;
-    vbM w2(in_place_type<maybe_throws_on_assign>(), 2);
-
-    ASSERT_EQ(0, maybe_throws_on_assign::dtor);
-
-    EXPECT_THROW(w2 = std::move(w1), nope);
-    EXPECT_FALSE(w2.valueless_by_exception());
-    EXPECT_EQ(0, maybe_throws_on_assign::dtor);
-}
-
-TEST(variant, equality) {
-    struct X {
-        int i;
-        X(int i): i(i) {}
-        X& operator=(const X&) { throw "nope"; }
-        bool operator==(X x) const { return i==x.i; }
-        // Crazy != semantics on purpose:
-        bool operator!=(X x) const { return i==x.i+1; }
-    };
-
-    ASSERT_TRUE(X{1} == X{1});
-    ASSERT_FALSE(X{1} == X{0});
-    ASSERT_FALSE(X{1} == X{2});
-
-    ASSERT_TRUE(X{1} != X{0});
-    ASSERT_FALSE(X{1} != X{1});
-    ASSERT_FALSE(X{1} != X{2});
-
-    using vidX = variant<int, double, X>;
-    auto valueless = []() {
-        vidX v{X{0}};
-        try { v = v; } catch (...) {};
-        return v;
-    };
-
-    EXPECT_TRUE(valueless() == valueless());
-    EXPECT_FALSE(valueless() != valueless());
-
-    EXPECT_TRUE(vidX{3} == vidX{3});
-    EXPECT_FALSE(vidX{3.0} == vidX{3});
-    EXPECT_FALSE(vidX{X{3}} == vidX{3});
-    EXPECT_FALSE(valueless() == vidX{3});
-
-    EXPECT_TRUE(vidX{X{2}} == vidX{X{2}});
-    EXPECT_FALSE(vidX{X{2}} == vidX{2});
-    EXPECT_FALSE(vidX{X{2}} == vidX{2.0});
-    EXPECT_FALSE(vidX{X{2}} == valueless());
-
-    EXPECT_FALSE(vidX{3} != vidX{3});
-    EXPECT_TRUE(vidX{3.0} != vidX{3});
-    EXPECT_TRUE(vidX{X{3}} != vidX{3});
-    EXPECT_TRUE(valueless() != vidX{3});
-
-    EXPECT_TRUE(vidX{X{2}} != vidX{X{1}});  // note custom !=
-    EXPECT_FALSE(vidX{X{2}} != vidX{X{2}}); // note custom !=
-    EXPECT_FALSE(vidX{X{2}} != vidX{X{3}}); // note custom !=
-    EXPECT_TRUE(vidX{X{2}} != vidX{2});
-    EXPECT_TRUE(vidX{X{2}} != vidX{2.0});
-    EXPECT_TRUE(vidX{X{2}} != valueless());
-}
-
-TEST(variant, hash) {
-    // Just ensure we find std::hash specializations.
-
-    std::hash<variant<>> h0;
-    EXPECT_TRUE((std::is_same<std::size_t, decltype(h0(std::declval<variant<>>()))>::value));
-
-    std::hash<variant<int, double>> h2;
-    (void)h2(variant<int, double>(3.1));
-    EXPECT_TRUE((std::is_same<std::size_t, decltype(h2(std::declval<variant<int, double>>()))>::value));
-}
-
-namespace {
-struct counts_swap {
-    static unsigned n_swap;
-    friend void swap(counts_swap&, counts_swap&) { ++counts_swap::n_swap; }
-};
-unsigned counts_swap::n_swap = 0;
-}
-
-TEST(variant, swap) {
-    struct X {
-        X() {}
-        X& operator=(const X&) { throw "nope"; }
-    };
-    using vidX = variant<int, double, X>;
-
-    auto valueless = []() {
-        vidX v{X{}};
-        try { v = v; } catch (...) {};
-        return v;
-    };
-
-    {
-        vidX a(valueless()), b(valueless());
-        ASSERT_TRUE(a.valueless_by_exception());
-        ASSERT_TRUE(b.valueless_by_exception());
-        std::swap(a, b);
-        EXPECT_TRUE(a.valueless_by_exception());
-        EXPECT_TRUE(b.valueless_by_exception());
-    };
-
-    {
-        vidX a(valueless()), b(3.2);
-        ASSERT_TRUE(a.valueless_by_exception());
-        ASSERT_EQ(1u, b.index());
-
-        std::swap(a, b);
-        EXPECT_TRUE(b.valueless_by_exception());
-        EXPECT_EQ(1u, a.index());
-        ASSERT_NE(nullptr, get_if<1>(a));
-        EXPECT_EQ(3.2, get<1>(a));
-    }
-
-    {
-        vidX a(1.2), b(3);
-        std::swap(a, b);
-
-        ASSERT_EQ(0u, a.index());
-        EXPECT_EQ(3, get<int>(a));
-
-        ASSERT_EQ(1u, b.index());
-        EXPECT_EQ(1.2, get<double>(b));
-    }
-
-    {
-        variant<counts_swap> y0, y1;
-        ASSERT_EQ(0u, counts_swap::n_swap);
-
-        std::swap(y0, y1);
-        EXPECT_EQ(1u, counts_swap::n_swap);
-    }
-}
-
-// Test generic accessors for pair, tuple.
-
-TEST(variant, get_pair_tuple) {
-    {
-        using pair_ni_nd = std::pair<nocopy<int>, nocopy<double>>;
-
-        nocopy<int>::reset_counts();
-        nocopy<double>::reset_counts();
-
-        auto f = first(pair_ni_nd{2, 3.4});
-        EXPECT_EQ(2, f.value);
-        EXPECT_EQ(1, nocopy<int>::move_ctor_count);
-
-        auto s = second(pair_ni_nd{2, 3.4});
-        EXPECT_EQ(3.4, s.value);
-        EXPECT_EQ(1, nocopy<double>::move_ctor_count);
-
-        nocopy<int>::reset_counts();
-        nocopy<double>::reset_counts();
-
-        auto g0 = ::arb::util::get<0>(pair_ni_nd{2, 3.4});
-        EXPECT_EQ(2, g0.value);
-        EXPECT_EQ(1, nocopy<int>::move_ctor_count);
-
-        auto g1 = ::arb::util::get<1>(pair_ni_nd{2, 3.4});
-        EXPECT_EQ(3.4, g1.value);
-        EXPECT_EQ(1, nocopy<double>::move_ctor_count);
-    }
-
-    {
-        struct X {};
-        using tuple_ni_nd_nx = std::tuple<nocopy<int>, nocopy<double>, nocopy<X>>;
-
-        nocopy<int>::reset_counts();
-        nocopy<double>::reset_counts();
-        nocopy<X>::reset_counts();
-
-        auto f = first(tuple_ni_nd_nx{2, 3.4, X{}});
-        EXPECT_EQ(2, f.value);
-        EXPECT_EQ(1, nocopy<int>::move_ctor_count);
-
-        auto s = second(tuple_ni_nd_nx{2, 3.4, X{}});
-        EXPECT_EQ(3.4, s.value);
-        EXPECT_EQ(1, nocopy<double>::move_ctor_count);
-
-        nocopy<int>::reset_counts();
-        nocopy<double>::reset_counts();
-
-        auto g0 = ::arb::util::get<0>(tuple_ni_nd_nx{2, 3.4, X{}});
-        EXPECT_EQ(2, g0.value);
-        EXPECT_EQ(1, nocopy<int>::move_ctor_count);
-
-        auto g1 = ::arb::util::get<1>(tuple_ni_nd_nx{2, 3.4, X{}});
-        EXPECT_EQ(3.4, g1.value);
-        EXPECT_EQ(1, nocopy<double>::move_ctor_count);
-
-        auto g2 = ::arb::util::get<2>(tuple_ni_nd_nx{2, 3.4, X{}});
-        (void)g2;
-        EXPECT_EQ(1, nocopy<X>::move_ctor_count);
-    }
-}