Is it possible to use std::visit on variant that hold same type more than once?
Asked Answered
E

2

6

For example, I'm trying to implement an AST using std::variant, where a Token can be a number, keyword, or variable. Where numbers are represented by the int and keywords and variables are represented by std::string:

enum TokenType : std::size_t {
    Number = 0, Keyword = 1, Variable = 2,
};
using Token = std::variant<int, std::string, std::string>;
Token token(std::in_place_index<TokenType::Variable>, "x");

When I want to do something with token, I can first determine its type with token.index() and then decide what to do:

switch (token.index()) {
case TokenType::Number:
    return do_sth_with_number(std::get<TokenType::Number>(token));
case TokenType::Keyword:
    return do_sth_with_keyword(std::get<TokenType::Keyword>(token));
case TokenType::Variable:
    return do_sth_with_variable(std::get<TokenType::Variable>(token));
}

However, I don't know if I can use std::visit to achieve the same effect. I only know that it can call specific function depending on the type of data in the variant, but I don't know if it is possible to do this depending on the index.

I know I can accomplish my goal by wrapping Keyword and Variable in two different classes, but I'm wondering if there's a better way to do it, because as I understand it, in variant, it should be more straightforward to decide which function to use based on the index rather than the type.

Extemporize answered 7/9, 2023 at 7:45 Comment(8)
The cleanest design is to have each kind of Token being represented by its own type and get rid of the TokenType enum altogether. The type of the token is the alternative held by the variant. No reason to save that same information in two different places.Dougdougal
Why not use an altetered version of std::visit? One that forwards index and the variable.Altair
Yes, std::visit dispatches the call based on the index but in all cases it calls the same visitor. So no, you cannot use std::visit if the visitor depends on the index, not just type.Feces
outside of generic code you shouldnt end up with two identical types in the variant to begin with. Make the type system your friend rather than fighting it and use a std::variant<Number, Keyword, Variable>Mollee
@Altair can you elaborate?Sold
@MarekR write an alternative version to std::visit that calls foo(index, var) or foo<index>(var) rather than foo(var). The implementation is pretty much the same as std::visit, one just needs to forward the index in addition.Altair
@Altair you've missed the point and just repleted same thing with more words. Where this is documented? Can you show demo on compiler explorer?Sold
@MarekR "elaborate" is for explain better. I had no intentions to write an implementation.Altair
S
2

It is possible to dispatch different functions based on the variant's index rather than the alternative type.

You can make the visitor accept an additional index constant to capture the index information of the current alternative, and then create a function table based on the size of the variant to forward different index constants and corresponding alternatives to the visitor

template<class Visitor, class Variant>
constexpr decltype(auto)
visit_i(Visitor&& vis, Variant&& var) {
  constexpr auto N = std::variant_size_v<std::remove_cvref_t<Variant>>;
  constexpr auto func_ptrs = []<std::size_t... Is>(std::index_sequence<Is...>) {
    return std::array{
      +[](Visitor& vis, Variant& var) -> decltype(auto) {
        return std::invoke(std::forward<Visitor>(vis), 
                           std::integral_constant<std::size_t, Is>{}, 
                           std::get<Is>(std::forward<Variant>(var)));
      }...
    };
  }(std::make_index_sequence<N>{});
  return func_ptrs[var.index()](vis, var);
}

Then you can visit the variant with a custom visitor that accepts an index constant

enum TokenType : std::size_t {
  Number = 0, Keyword = 1, Variable = 2,
};

using Token = std::variant<int, std::string, std::string>;

void do_sth_with_number(int);
void do_sth_with_keyword(std::string);
void do_sth_with_variable(std::string);

auto visitor = [](auto index, auto&& token) {
  if constexpr (index == TokenType::Number)
    return do_sth_with_number(token);
  else if constexpr (index == TokenType::Keyword)
    return do_sth_with_keyword(token);
  else if constexpr (index == TokenType::Variable)
    return do_sth_with_variable(token);
};

int main() {
  Token token(std::in_place_index<TokenType::Variable>, "x");
  visit_i(visitor, token);
}

Demo

Saintjust answered 7/9, 2023 at 8:35 Comment(0)
S
1

IMO there is no need to make things over-complicated:

void foo(const MyVariant& v)
{
    auto index = v.index();
    std::visit([index](const auto& x) {
        std::cout << "Index: " << index << " value: " << x << '\n';
    }, v);
}

https://godbolt.org/z/EfET9s97c

but IMHO comment under question from ComicSansMS is best solution.

Sold answered 7/9, 2023 at 9:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.