Can I extend variant in C++?
Asked Answered
B

4

18

I'm not sure that this is possible, but say I have:

using my_variant = std::variant<Class1, Class2, Class3>;

Now at some point, I create a Class4 and would like to extend my_variant2 to include all of my_variant along with Class4 (in a general way, i.e. not just using another using...) so that I can do something like create an array std::array<my_variant2, n>.

Is this something that can be done?

Baikal answered 18/9, 2018 at 20:2 Comment(0)
C
16

godbolted

#include <variant>

template <typename T, typename... Args> struct concatenator;

template <typename... Args0, typename... Args1>
struct concatenator<std::variant<Args0...>, Args1...> {
    using type = std::variant<Args0..., Args1...>;
};

int main() {
    using type_t = std::variant<int, char, double>;
    static_assert(
        std::is_same_v<
            concatenator<type_t, float, short>::type,
            std::variant<int, char, double, float, short>>);
    return 0;
}
Clearwing answered 18/9, 2018 at 20:13 Comment(4)
Simple and straightforward. I'm surprised the most complex answers have more upvotes than this.Infrequent
@CássioRenan - I know!Clearwing
Interesting. This makes, I guess, std::variant into a noncommutative monoid with identity std::variant<>.Baikal
@Baikal - made me google what monoid is (finally), but agree, seems that way, like any other variadic template, in principle.Clearwing
C
11

There are two steps to achieving this. The first is to identify the list of types used in the original std::variant and the second is to construct a new std::variant type with the original arguments plus the one to add.

Partial template specialization can be used to write a trait that will obtain the list of template types used in a given std::variant :

#include <variant>

template<class T>
struct t_variant_cat;

template<class ... Old>
struct t_variant_cat<std::variant<Old...>> {
    // Old is a parameter pack containing all of 
    //  template arguments of the std::variant
};

Next we add another template argument that specifies the type to add and define an alias for this new type.

#include <variant>

template<class T, class New>
struct t_variant_cat;

template<class ... Old, class New>
struct t_variant_cat<std::variant<Old...>, New> {
    using type = std::variant<Old..., New>;
};

typename t_variant_cat<my_variant, Class4>::type should now yield std::variant<Class1, Class2, Class3, Class4>. For convenience we can add a type alias to avoid having to write typename and ::type each time :

template<class Old, class New>
using t_variant_cat_t = typename t_variant_cat<Old, New>::type;

The usage would be :

#include <variant>

template<class T, class New>
struct t_variant_cat;

template<class ... Old, class New>
struct t_variant_cat<std::variant<Old...>, New> {
    using type = std::variant<Old..., New>;
};

template<class Old, class New>
using t_variant_cat_t = typename t_variant_cat<Old, New>::type;

using old = std::variant<int, float>;
using extended = t_variant_cat_t<old, double>;

// Makes sure this actually works
static_assert(std::is_same_v<extended, std::variant<int, float, double>>,
    "Something is wrong.");
Colobus answered 18/9, 2018 at 20:15 Comment(2)
The question is incorrectly tagged with C++14 (although std::variant is only there in C++17). Assuming that, you can chop 7 bytes from your example by using std::is_same_v instead of std::is_same<>::value, and another 2 bytes by not defining the t_variant_cat, it only needs to be declared :-DClearwing
@Clearwing Thank you for your feedback. I'll make that change.Anthropolatry
U
8
namespace impl_details {
  template<class Var, class...Ts>
  struct extend_type;
  template<template<class...>class Z, class...Vs, class...Ts>
  struct extend_type<Z<Vs...>, Ts...> {
    using type=Z<Vs..., Ts...>;
  };
}
template<class Var, class...Ts>
using extend_type = typename impl_details::extend_type<Var, Ts...>::type;

now

extend_type<my_variant, Class4>

is

std::variant<Class1, Class2, Class3, Class4>

althought I disapprove of your 1-based indexing.

extend_type< std::tuple<a,b,c>, d, e, f > also works.

I can have some fun with this...

namespace impl_details {
  template<class Lhs, class Rhs>
  struct type_cat;
  template<template<class...>class Z, class...Lhs, class...Rhs>
  struct type_cat<Z<Lhs...>, Z<Rhs...>> {
    using type=Z<Lhs..., Rhs...>;
  };
}
template<class Lhs, class Rhs>
using type_cat = typename impl_details::type_cat<Lhs, Rhs>::type;


auto variant_trinary( bool b ) {
  return [b](auto&& lhs, auto&& rhs) {
    using R=type_cat< std::decay_t<decltype(lhs)>, std::decay_t<decltype(rhs)> >;
    auto as_R = [](auto&&x)->R{ return decltype(x)(x)); };
    if (b)
      return std::visit( as_R, lhs );
    else
      return std::visit( as_R, rhs );
  };
}

which gives us the trinary operator on two variants.

auto var = variant_trinary(bool_expr)( var1, var2 );

where var is the concatination of the variant types of var1 and var2.

Unplug answered 18/9, 2018 at 20:14 Comment(0)
D
1

bobah's answer here is very helpful, but it wasn't clear to me how to handle the runtime part of this, of how to actually merge these objects at runtime.

Obviously a big if-chain is an option, but there's actually a different, more concise, way to do this:

#include <iostream>
#include <variant>

template <typename T, typename... Args> struct concatenator;

template <typename... Args0, typename... Args1>
struct concatenator<std::variant<Args0...>, Args1...> {
    using type = std::variant<Args0..., Args1...>;
};

int main() {
    using typea_t = std::variant<int, char, double>;
    using typeb_t = concatenator<typea_t, float, short>::type;

    float foo = 0.1f;
    typea_t bar{12.0};

    typeb_t foobar = foo;
    std::visit([&](auto&& value) { foobar = value; }, bar);

    std::visit([](auto&& value) { std::cout << value << std::endl; }, foobar);

    return 0;
}

godbolt

Defunct answered 22/4, 2023 at 19:37 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.