5

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?

fabian
  • 80,457
  • 12
  • 86
  • 114
Sven
  • 141
  • 5
  • You do need to pass information about the variant content type you want to access: the return type of a function can only be conditionally different, if you use `auto`+`if constexpr`, but in this case the real type stored in the vector element is not a compile time constant and therefore cannot be used in the condition of a `if constexpr()`. If you do know the type(index) of the stored type though, you can use [`return std::get(m_data[index]);` or `return std::get(m_data[index]);`](https://en.cppreference.com/w/cpp/utility/variant/get) – fabian Aug 28 '22 at 12:50
  • @fabian So basically I have to add another member to my struct which holds information about the type of a given index in my std::vector? – Sven Aug 28 '22 at 13:07
  • 1
    This question and your other one (https://stackoverflow.com/questions/73518123/overload-subscript-operator-for-stdany) implies a misunderstanding of runtime vs compile time information. You'd need to be able to answer the question "What is the type of `d[n]`" - if the type is not known until runtime, you have a problem (or rather, you have a variant). You can't say for example `auto x = d[n]` and expect the type of `x` to be anything other than the variant itself--certainly not the specific type held by the variant, as that is not known at compile time. – John Zwinck Aug 28 '22 at 13:07
  • 2
    @Niklas: No, the variant knows what type it holds, you do not need to store it separately. – John Zwinck Aug 28 '22 at 13:07
  • @JohnZwinck so I have to cast `d[n]` to the expected return type (which is expected at compile time) as I did in the approach (which isnt working) mentioned in my original post? – Sven Aug 28 '22 at 13:26
  • What will work is `std::get(m_data[0])`. See how you're explicitly telling the compiler what type you expect? I see your `operator[]` proposal but I don't see how you expect to be able to invoke it--perhaps you can add that to your question. – John Zwinck Aug 28 '22 at 13:29
  • @JohnZwinck Thanks, so `std::get(m_data[0])` expects an `int` at index `0`, so if I don't know which data type is currently at index `0` I have to expand my structure to keep track of which type is at which position, isn't it? – Sven Aug 28 '22 at 14:01

3 Answers3

12

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
6

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[].

anatolyg
  • 26,506
  • 9
  • 60
  • 134
1

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'; });
}
Aykhan Hagverdili
  • 28,141
  • 6
  • 41
  • 93
  • Mandatory [Thou shalt not inherit from std::vector](https://stackoverflow.com/questions/4353203/thou-shalt-not-inherit-from-stdvector). Doing sets up a trap due to object slicing and due to `std::vector` not having a `virtual` destructor. – François Andrieux Aug 28 '22 at 14:23
  • @FrançoisAndrieux I don't agree. It's OK in this context, as long as you don't do polymorphism. Non-polymorphic inheritance is actually a very common pattern. Just look around libstdc++ implementation code. `std::vector` is a subtype of `std::_Vector_base ` even though neither has a virtual destructor. – Aykhan Hagverdili Aug 28 '22 at 14:26
  • @FrançoisAndrieux Please also note that the slicing issue has absolutely nothing to do with the existence of a virtual destructor. – Aykhan Hagverdili Aug 28 '22 at 19:20