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); + } +}