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?

7
  • 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<T>(m_data[index]); or return std::get<TypeIndex>(m_data[index]); Commented Aug 28, 2022 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<std::variant>? Commented Aug 28, 2022 at 13:07
  • 1
    This question and your other one (stackoverflow.com/questions/73518123/…) 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. Commented Aug 28, 2022 at 13:07
  • 2
    @Niklas: No, the variant knows what type it holds, you do not need to store it separately. Commented Aug 28, 2022 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? Commented Aug 28, 2022 at 13:26

3 Answers 3

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.

Sign up to request clarification or add additional context in comments.

Comments

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

3 Comments

What woud be the way to go if I don't know that there is a double at index 2 at compile time?
@Niklas my answer...
@Niklas Types are determined at compile time. If you don't know that index 2 has a double you can't exploit that information at compile time.
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'; });
}

3 Comments

Mandatory Thou shalt not inherit from std::vector. Doing sets up a trap due to object slicing and due to std::vector not having a virtual destructor.
@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.
@FrançoisAndrieux Please also note that the slicing issue has absolutely nothing to do with the existence of a virtual destructor.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.