diff --git a/arbor/include/arbor/util/variant.hpp b/arbor/include/arbor/util/variant.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..09700a1c30cf4793d9ff7ad0f70b8524da023157
--- /dev/null
+++ b/arbor/include/arbor/util/variant.hpp
@@ -0,0 +1,607 @@
+#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, const 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) {}
+};
+
+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);
+        }
+        if (i!=std::size_t(-1)) throw bad_variant_access{};
+    }
+
+    static void move_assign(std::size_t i, char* data, const char* from) {
+        if (i==0) {
+            *reinterpret_cast<H*>(data) = std::move(*reinterpret_cast<const 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);
+        }
+    }
+};
+
+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 {
+            which_ = npos;
+            if (x.which_!=npos) {
+                variant_dynamic_impl<T...>::assign(x.which_, data, x.data);
+                which_ = x.which_;
+            }
+        }
+        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 {
+            which_ = npos;
+            if (x.which_!=npos) {
+                variant_dynamic_impl<T...>::move_assign(x.which_, data, x.data);
+                which_ = x.which_;
+            }
+        }
+        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<>(): 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>(); }
+};
+
+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...>>; };
+
+template <typename Visitor, typename... Variant>
+using visit_return_t = decltype(std::declval<Visitor>()(std::declval<typename variant_alternative<0, std::remove_volatile_t<std::remove_reference_t<Variant>>>::type>()...));
+
+} // 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 = detail::visit_return_t<Visitor&&, Variant&&>;
+
+    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) {
+        return v.index() ^
+            visit([](const auto& a) { return std::hash<std::remove_cv_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) { 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/util/meta.hpp b/arbor/util/meta.hpp
index 957c3451c234fa59c584794cba21b24a2b839d75..4ab20318d488d5ae36ed3620adbfa4b1c5e64e03 100644
--- a/arbor/util/meta.hpp
+++ b/arbor/util/meta.hpp
@@ -2,8 +2,11 @@
 
 /* Type utilities and convenience expressions.  */
 
+#include <array>
 #include <cstddef>
 #include <iterator>
+#include <tuple>
+#include <utility>
 #include <type_traits>
 
 namespace arb {
@@ -269,34 +272,19 @@ template <typename I, typename E>
 struct has_common_random_access_iterator<I, E, void_t<util::common_random_access_iterator_t<I, E>>>:
     std::true_type {};
 
-// No generic lambdas in C++11, so some convenience accessors for pairs that
-// are type-generic
+// Generic accessors:
+//    * first and second for pairs and tuples;
+//    * util::get<I> to forward to std::get<I> where applicable, but
+//      is otherwise extensible to non-std types.
 
-struct first_t {
-    template <typename U, typename V>
-    U& operator()(std::pair<U, V>& p) {
-        return p.first;
-    }
+static auto first = [](auto&& pair) -> decltype(auto) { return std::get<0>(std::forward<decltype(pair)>(pair)); };
+static auto second = [](auto&& pair) -> decltype(auto) { return std::get<1>(std::forward<decltype(pair)>(pair)); };
 
-    template <typename U, typename V>
-    const U& operator()(const std::pair<U, V>& p) const {
-        return p.first;
-    }
-};
-constexpr first_t first{};
+template <typename X, typename U>
+decltype(auto) get(U&& u) { return std::get<X>(std::forward<U>(u));}
 
-struct second_t {
-    template <typename U, typename V>
-    V& operator()(std::pair<U, V>& p) {
-        return p.second;
-    }
-
-    template <typename U, typename V>
-    const V& operator()(const std::pair<U, V>& p) const {
-        return p.second;
-    }
-};
-constexpr second_t second{};
+template <std::size_t I, typename U>
+decltype(auto) get(U&& u) { return std::get<I>(std::forward<U>(u));}
 
 } // namespace util
 } // namespace arb
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 93b92fa8708042f4c439f0278f7b0b6a293a7354..f97bb2d3127688e8cefc639301a524b32552cdc5 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -145,6 +145,7 @@ set(unit_sources
     test_transform.cpp
     test_uninitialized.cpp
     test_unique_any.cpp
+    test_variant.cpp
     test_vector.cpp
     test_version.cpp
 
diff --git a/test/unit/common.hpp b/test/unit/common.hpp
index 2436caa3866361183ecdda7116e56b4df45f5b2b..54db33d3aa75436913a37044efd1f244badd8019 100644
--- a/test/unit/common.hpp
+++ b/test/unit/common.hpp
@@ -7,6 +7,7 @@
 
 #include <cmath>
 #include <string>
+#include <type_traits>
 #include <utility>
 
 #include "../gtest.h"
@@ -44,30 +45,49 @@ struct null_terminated_t {
 
 constexpr null_terminated_t null_terminated;
 
+template <typename... A>
+struct matches_cvref_impl: std::false_type {};
+
+template <typename X>
+struct matches_cvref_impl<X, X>: std::true_type {};
+
+template <typename... A>
+using matches_cvref = matches_cvref_impl<std::remove_cv_t<std::remove_reference_t<A>>...>;
+
 // Wrap a value type, with copy operations disabled.
 
 template <typename V>
 struct nocopy {
     V value;
 
-    nocopy(): value{} {}
-    nocopy(V v): value(v) {}
+    template <typename... A>
+    using is_self = matches_cvref<nocopy, A...>;
+
+    template <typename... A, typename = std::enable_if_t<!is_self<A...>::value>>
+    nocopy(A&&... a): value(std::forward<A>(a)...) {}
+
+    nocopy(nocopy& n) = delete;
     nocopy(const nocopy& n) = delete;
 
-    nocopy(nocopy&& n) {
-        value=n.value;
-        n.value=V{};
+    nocopy(nocopy&& n): value(std::move(n.value)) {
+        n.clear();
         ++move_ctor_count;
     }
 
     nocopy& operator=(const nocopy& n) = delete;
     nocopy& operator=(nocopy&& n) {
-        value=n.value;
-        n.value=V{};
+        value = std::move(n.value);
+        n.clear();
         ++move_assign_count;
         return *this;
     }
 
+    template <typename U = V>
+    std::enable_if_t<std::is_default_constructible<U>::value> clear() { value = V{}; }
+
+    template <typename U = V>
+    std::enable_if_t<!std::is_default_constructible<U>::value> clear() {}
+
     bool operator==(const nocopy& them) const { return them.value==value; }
     bool operator!=(const nocopy& them) const { return !(*this==them); }
 
@@ -91,18 +111,19 @@ template <typename V>
 struct nomove {
     V value;
 
-    nomove(): value{} {}
-    nomove(V v): value(v) {}
-    nomove(nomove&& n) = delete;
+    template <typename... A>
+    using is_self = matches_cvref<nomove, A...>;
 
-    nomove(const nomove& n): value(n.value) {
-        ++copy_ctor_count;
-    }
+    template <typename... A, typename = std::enable_if_t<!is_self<A...>::value>>
+    nomove(A&&... a): value(std::forward<A>(a)...) {}
+
+    nomove(nomove& n): value(n.value) { ++copy_ctor_count; }
+    nomove(const nomove& n): value(n.value) { ++copy_ctor_count; }
 
     nomove& operator=(nomove&& n) = delete;
 
     nomove& operator=(const nomove& n) {
-        value=n.value;
+        value = n.value;
         ++copy_assign_count;
         return *this;
     }
diff --git a/test/unit/test_variant.cpp b/test/unit/test_variant.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e8ca9331371bef4f18a802ec5367d940341a5e36
--- /dev/null
+++ b/test/unit/test_variant.cpp
@@ -0,0 +1,405 @@
+#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());
+}
+
+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;
+    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);
+    }
+}