Access std::vector<std::variant> value by index

Viewed 136

I would like to access a member of std::vector<std::variant> by index. Considering the following snippet:

struct Data {
    
  using data_types = std::variant<std::basic_string<char>, double, int>;
    
  public:
  
  template <class T>
  void push_back(const T& t) {
    m_data.push_back(t);    
  }
  
  private:
  
  std::vector<data_types> m_data;
 
};


int main()
{
 Data d;
 d.push_back(0);
 d.push_back("string");
 d.push_back(3.55);
}

I would like to access the values like d[0] (should return int) or d[1] (should return std::string).

What I have tried so far but what isn't working is to add the following public method to the existing struct:

  template <class T>
  T& operator[](const size_t &index) {
      return std::visit([](const T& value) {
          return static_cast<T>(value);
      }, m_data[index]);
  }

Any ideas how to achieve the desired result?

3 Answers

The type of an expression in C++ cannot depend on runtime parameters; basically it can only depend on types of the arguments, plus non-type template arguments.

So d[0] and d[1] must have the same type, as the type of the pieces of the expression are identical, and there are no non-type template arguments.

std::get<int>(d[0]) vs std::get<double>(d[1]) can differ in type.

std::get<1>(d[0]) vs std::get<2>(d[1]) can differ in type.

std::visit is a mechanism used to get around this; here, we create every a function object call, one for each possible type, and then pick one at runtime to actually call. However, the type returned from the visit still follows the above rule: it doesn't depend on what type is stored in the variant, and every possible type in the variant must have a valid instantiation of the function.

C++ type system is not a runtime type system. It is compile-time. Stuff like variant and dynamic_cast and any give some runtime exposure to it, but it is intentionally minimal.

If you are wanting to print the contents of a variant, you can do this:

std::visit([](auto& x){
  std::cout << x;
}, d[0]);

the trick here is that each of the various types of variant have a lambda function body written for them (so they all must be valid). Then, at run time, the one actually in the variant is run.

You can also test the variant and ask if it has a specific type, either via std::get or manually.

bool has_int = std::visit([](auto& x){
  return std::is_same_v<int, std::decay_t<decltype(x)>>::value;
}, d[0]);

this gives you a bool saying if d[0] has an int in it or not.


The next bit is getting insane. Please don't read this unless you fully understand how to use variants and want to know more:

You can even extract out the type index of the variant and pass that around as a run time value:

template<auto I>
using konstant_t = std::integral_constant<decltype(I),I>;
template<auto I>
constexpr konstant_t<I> konstant_v{};

template<auto...Is>
using venum_t = std::variant< konstant_t<Is>... >;

template<class Is>
struct make_venum_helper;
template<class Is>
using make_venum_helper_t = typename make_venum_helper<Is>::type;
template<std::size_t...Is>
struct make_venum_helper<std::index_sequence<Is...>>{
  using type=venum_t<Is...>;
};


template<std::size_t N>
using make_venum_t = typename make_venum_helper<std::make_index_sequence<N>>::type;

template<std::size_t...Is>
constexpr auto venum_v( std::index_sequence<Is...>, std::size_t I ) {
  using venum = make_venum_t<sizeof...(Is)>;
  constexpr venum arr[]={
    venum( konstant_v<Is> )...
  };
  return arr[I];
}
template<std::size_t N>
constexpr auto venum_v( std::size_t I ) {
  return venum_v( std::make_index_sequence<N>{}, I );
}

template<class...Ts>
constexpr auto venum_v( std::variant<Ts...> const& v ) {
  return venum_v< sizeof...(Ts) >( v.index() );
}

now you can do this:

using venum = make_venum_t<3>;
venum idx = venum_v(d[0]);

and idx holds the index of the engaged type in d[0]. This is only somewhat useful, as you still need std::visit to use it usefully:

std::visit([&](auto I) {
  std::cout << std::get<I>( d[0] );
}, idx );

(within the lambda, I is a std::integral_constant, which can be constexpr converted to an integer.)

but lets you do some interesting things with it.

To extract a value from variant, use std::get:

struct Data
{
    ...
    template <class T>
    T& operator[](size_t index)
    {
        return std::get<T>(m_data[index]);
    }
};

However, because this overloaded operator is a template, you can't use simple operator syntax to call it. Use the verbose syntax:

int main()
{
 Data d;
 d.push_back(0);
 d.push_back("string");
 d.push_back(3.55);
 std::cout << d.operator[]<double>(2);
}

Or rename it to use a plain name instead of the fancy operator[].

Visitor pattern:

#include <iostream>
#include <string>
#include <variant>
#include <vector>

template <class ...Ts>
struct MultiVector : std::vector<std::variant<Ts...>> {
    template <class Visitor>
    void visit(std::size_t i, Visitor&& v) {
        std::visit(v, (*this)[i]);
    }
};

int main() {
    MultiVector<std::string, int, double> vec;
    vec.push_back(0);
    vec.push_back("string");
    vec.push_back(3.55);

    vec.visit(2, [](auto& e) { std::cout << e << '\n'; });
}
Related