Home > Net >  Correct use of std::variant and std::visit when functor requires multiple arguments
Correct use of std::variant and std::visit when functor requires multiple arguments

Time:01-30

I have been struggling a bit to get my code using std::variant in combination with std::visit to work. I have now reduced my code to a minimum of just using one type in my variant and still the compiler complains about no matching call to visit.

#include <iostream>
#include <variant>
#include <vector>
#include <array>
class SomeClass {
    
    public:
    
    std::vector<int> foo(const std::string& s, const std::array<float, 3>& d){
        return {};
    }
};

struct Visitor{
    public:
    
    std::vector<int> operator() (SomeClass& c1, const std::string& s, const std::array<float, 3>& a) {
        
        return c1.foo(s, a);        
        
    }
};

int main() {
    
    std::variant<SomeClass> variants;
    
    std::string s = "c  ";
    std::array<float, 3> a {1, 0, 0};
    
    std::visit(Visitor{}, variants, s, a);

    return 0;
}

In am using gcc 11.3

CodePudding user response:

std::visit expects every argument after the visitor to be a variant. It will "unwrap" every variant to the underlying object and then make a call to the visitor with all of those objects. It will give an error if you try to give it non-variants like strings.

A visitor can have its own internal state/variables. You want to bring s and a into your visitor. That is, s and a are not conceptually arguments to the visitor. You should think of them as part of the visitor, since the visitor object represents "what to do to the variant" and s and a are parts of that. s and a are not inputs to the visitor because the input to the visitor should be the variant.

struct Visitor {
    std::string s;
    std::array<float, 3> a;
    std::vector<int> operator()(SomeClass &c) {
        return c.foo(s, a);
    }
};

std::variant<SomeClass> variants;
Visitor v{"c  ", {1, 0, 0}};
std::visit(v, variants);
// you *could* shorten this to a lambda with captures
// std::string s = "c  "
// std::array<float, 3> a{1, 0, 0};
// std::visit([&](SomeClass &c) { return c.foo(s, a); }, variants);
// but this will not generalize to multiple things in the variant (lambdas don't overload)

There is also the following hack. I would consider it bad style (since it doesn't correctly separate the visitor from what's being visited), but if you must use it works.

template<typename T>
std::variant<std::reference_wrapper<T>> unvisit(T &x) {
    return std::ref(x);
}
// using your original Visitor this time
std::variant<SomeClass> variants;
std::string s = "c  ";
std::array<float, 3> a {1, 0, 0};

std::visit(Visitor{}, variants, unvisit(s), unvisit(a));

I.e. you may wrap every "non-variant" argument into a single-variant variant by reference and then rely on visit to unwrap it again.

CodePudding user response:

std::visit calls the visitor by always passing a single argument to the visitor: the object the visitor is visiting.

Additional parameters to std::visit are additional variants to visit.

std::visit(Visitor{}, variants, s, a);

This means: use the visitor{} to visit variants, then visit s, then finally a. All of these are expected to be, themselves, variants.

In other words: std::visit does not work the way you thought it worked. You'll need to come up with some other, alternative, means to visit your variants.

  • Related