1

In C++17 how can one verify in a constexpr that a type belongs to the typelist of a variant ?

e.g:

using MyVt = std::variant<int, float>;
static_assert( MyVt::has_type< bool >::value, "oops, forgot bool");

or

static_assert( mpl::has_key< MyVt::typelist, T >::value, "oops, forgot T");

Of course more useful in concept expressions, or just as static_assert in a template function; to restrict the possible types accepted.

If we don't have access to an explicitly supported standard metafunction or metalist for this, would it be possible to hack a check using SFINAE involving a constructor expression ?

Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
v.oddou
  • 6,476
  • 3
  • 32
  • 63

4 Answers4

5

The basic solution uses a fold expression (C++17) and partial specialization:

#include <type_traits>
#include <variant>

template<class T, class TypeList>
struct IsContainedIn;

template<class T, class... Ts>
struct IsContainedIn<T, std::variant<Ts...>>
  : std::bool_constant<(... || std::is_same<T, Ts>{})>
{};

using MyVt = std::variant<int, float>;
static_assert(IsContainedIn<bool, MyVt>::value, "oops, forgot bool");

You can make it more generic by using a template template parameter. This way, it also works for std::tuple, std::pair, and other templates. Those other templates must use only type template parameters, though (e.g., std::array does not match the template template parameter template<class...> class Tmpl in the example below).

template<class T, template<class...> class Tmpl, class... Ts>
struct IsContainedIn<T, Tmpl<Ts...>>
  : std::bool_constant<(... || std::is_same<T, Ts>{})>
{};

Finally, this good C++17 answer to a C++11 question uses std::disjunction instead of a fold expression. You can think of std::disjunction as the functional any_of. This enables short-circuit evaluation (at compile time). In this case it reads

template<class T, template<class...> class Tmpl, class... Ts>
struct IsContainedIn<T, Tmpl<Ts...>>
  : std::disjunction<std::is_same<T, Ts>...>
{};

The cppreference notes on std::disjunction state that

[...]

The short-circuit instantiation differentiates disjunction from fold expressions: a fold expression like (... || Bs::value) instantiates every B in Bs, while std::disjunction_v<Bs...> stops instantiation once the value can be determined. This is particularly useful if the later type is expensive to instantiate or can cause a hard error when instantiated with the wrong type.

Julius
  • 1,816
  • 10
  • 14
  • +1 for the "more generic" solution and for the use of `std::disjunction`; I think you should emphasize that the "more generic" solution is perfect to respond to the same question regarding `std::tuple`. – max66 Dec 12 '18 at 14:01
  • p.s.: anyway, as far I know, also fold expression enables short-circuit evaluation; even so, IMHO, `std::disjunction` is better because is more expressive and simpler to understand. – max66 Dec 12 '18 at 17:27
  • I'm shocked (nicely). the compiler can remember the original (canonical type) template type and destructure it like this ? – v.oddou Dec 13 '18 at 02:57
  • Took me a while in cppreference to understand that `std::disjunction` is the functional `any` and `conjuction` is `all`. I see. – v.oddou Dec 13 '18 at 05:41
  • wait a minute, I just understood the trick. this is a specialization, so the template pattern matching was already a thing of C++98. Ooooh nice. accepted ! – v.oddou Dec 13 '18 at 07:37
  • @max66: Have you read the quote from cppreference.com on fold expression vs. `std::disjunction`? In accordance to cppreference.com, I [experience that fold expressions do not short-circuit](https://godbolt.org/z/qFMdek). – Julius Dec 13 '18 at 08:23
  • D'Oh! You're right. I believed otherwise but you example prove that I'm wrong. Seems that I've read superficially [this answer](https://stackoverflow.com/questions/50111274/are-fold-expressions-subject-to-short-circuiting/50115876#50115876). The difference is that short circuiting is enabled compile time for `std::disjunction` and run-time for template folding? Anyway, thanks for pointing it. – max66 Dec 13 '18 at 14:18
1

Not a great difference but an alternative to the Julius's answer can the use of the same check (std::bool_constant<(... || std::is_same<T, Ts>{}) or, better, std::disjunction<std::is_same<T, Ts>...>) be the same things through the declaration of a constexpr function and a template constexpr variable

template <typename T, template <typename...> class C, typename ... Ts>
constexpr auto isTypeInList (C<Ts...> const &)
    -> std::disjunction<std::is_same<T, Ts>...>;

template <typename T, typename V>
static constexpr bool isTypeInList_v 
   = decltype(isTypeInList<T>(std::declval<V>()))::value;

and you can use they as follows

using MyVt = std::variant<int, float>;

static_assert( isTypeInList_v<int, MyVt> );
static_assert( isTypeInList_v<double, MyVt> == false );

Not a great improvement but... if you also define (non only declare) the isTypeInList() function

template <typename T, template <typename...> class C, typename ... Ts>
constexpr auto isTypeInList (C<Ts...> const &)
    -> std::disjunction<std::is_same<T, Ts>...>
 { return {}; } 

you can also use it directly to check objects

MyVt myVar {0};

static_assert( isTypeInList<int>(myVar) );

avoiding the need of pass through a decltype()

MyVt myVar {0};

static_assert( isTypeInList_v<int, decltype(myVar)> );
max66
  • 65,235
  • 10
  • 71
  • 111
1

What I love about Boost.Mp11 is that the answer to seemingly every question is a one-liner. In this case, mp_contains:

static_assert(mp_contains<MyVt, bool>, "oops, forgot bool");

It's a header-only, standalone library. It is great. Note that this works for tuple as well as variant.


You can approximate the approach via the following:

template <typename L, typename V> struct mp_contains_impl;
template <template<typename...> class L, typename... Ts, typename V>
struct mp_constaints_impl<L<Ts...>, V>
    : std::integral_constant<bool,
        (std::is_same_v<Ts, V> || ... )>
{ };

template <typename L, typename V>
using mp_contains = typename mp_contains_impl<L, V>::type;
Barry
  • 286,269
  • 29
  • 621
  • 977
1

An entirely different approach is to create a type that is only convertible to exactly the type you're looking for, and see if you can construct your variant from it:

template <typename T>
struct ConvertsTo {
    template <typename U,
        std::enable_if_t<std::is_same_v<T,U>, int> = 0>
    operator U() const;
};

template <typename V, typename T>
using variant_contains = std::is_constructible<V, ConvertsTo<T>>;

static_assert(variant_contains<std::variant<int, double>, int>::value);
static_assert(!variant_contains<std::variant<int, double>, bool>::value);
Barry
  • 286,269
  • 29
  • 621
  • 977
  • nice ! though I am not completely certain that the `std::variant` implementation will activate substitution failure directly in the templates of its constructor. If they apply the verification of the type membership in a `static_assert` later in the code body (depending on stdlib implementation detail) , then it will substitute fine, and `std::is_constructible` will claim `yes` when in fact, not so much... – v.oddou Dec 13 '18 at 06:48
  • 1
    @v.oddou Variant is _required_ to have its converting constructor only participate in overload resolution if there is a unique viable conversion. The strategy you're describing would be nonconforming. – Barry Dec 13 '18 at 12:42