6

I want to execute an overloaded function over a variant. The following code block works and compiles, but the visit invocation seems overly complex. Why can I not simply write:

std::visit(&f, something);

Working version and context:

#include <variant>
#include <string>
#include <iostream>
#include <functional>

struct A {
        std::string name = "spencer";
};

struct B {
        std::string type = "person";
};

struct C {
        double age = 5;
};

void f(A a) {
        std::cout << a.name << std::endl;
}

void f(B b) {
        std::cout << b.type << std::endl;
}

void f(C c) {
        std::cout << c.age << std::endl;
}


int main() {
        std::variant<A, B, C> something{B{}};
        std::visit([](auto&& x) {f(x);}, something);
}

Is there a simpler way?

Hurricane Development
  • 2,449
  • 1
  • 19
  • 40
  • There's been proposals kicking around for this. https://wg21.link/p1170 is the latest AFAIK. – Justin Apr 06 '21 at 01:53
  • 4
    Does this answer your question? [Is there a clean way to turn an overload set into a visitor suitable for use with std::visit?](https://stackoverflow.com/questions/66391686/is-there-a-clean-way-to-turn-an-overload-set-into-a-visitor-suitable-for-use-wit) – Justin Apr 06 '21 at 01:54

3 Answers3

8
std::visit(&f, something);

This is not valid, because f is not a single function. &f says "give me a pointer to f". But f isn't one thing; it's three functions which happen to share a name, with three separate pointers.

std::visit([](auto&& x) {f(x);}, something);

This creates a closure based on a template which generates code to do dispatch at compile-time. Effectively, it works as though we did

void f(A a) {
  std::cout << a.name << std::endl;
}

void f(B b) {
  std::cout << b.type << std::endl;
}

void f(C c) {
  std::cout << c.age << std::endl;
}

struct F {  
  template<typename T>
  void operator()(T x) {
    f(x);
  }
};

int main() {
  std::variant<A, B, C> something{B{}};
  std::visit(F(), something);
}

Which would force the C++ compiler, during template expansion, to produce something like

void f(A a) {
  std::cout << a.name << std::endl;
}

void f(B b) {
  std::cout << b.type << std::endl;
}

void f(C c) {
  std::cout << c.age << std::endl;
}

struct F {
  void operator()(A x) {
    f(x);
  }
  void operator()(B x) {
    f(x);
  }
  void operator()(C x) {
    f(x);
  }
};

int main() {
  std::variant<A, B, C> something{B{}};
  std::visit(F(), something);
}

If you want to eliminate the lambda wrapper, you need a single callable object that can be passed as an argument, and a function pointer will not suffice, as a function pointer can't do overload resolution. We can always make a functor object explicitly.

struct F {
  void operator()(A a) {
    std::cout << a.name << std::endl;
  }
  void operator()(B b) {
    std::cout << b.type << std::endl;
  }
  void operator()(C c) {
    std::cout << c.age << std::endl;
  }
};

int main() {
  std::variant<A, B, C> something{B{}};
  std::visit(F(), something);
}

It's up to you whether or not you consider this cleaner than the previous approach. On the one hand, it's more like the traditional OOP visitor pattern, in that we have an object doing the visiting. On the other hand, it would be nice if we could pass the name of a function and have C++ understand what we mean, but that would either require special C++ syntax for std::visit or runtime-dispatch in the form of multimethods. Either way, it's unlikely to happen soon, or at all.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
3

Is there a simpler way? Well, there might be a syntactically more streamlined way:

  1. You can use overloaded{} from the cppreference example right inside the std::visit:
std::visit(overloaded{
    [](A a) { std::cout << a.name << std::endl; },
    [](B b) { std::cout << b.type << std::endl; },
    [](C c) { std::cout << c.age << std::endl; }
}, something);

here, the overloaded implementation is pretty simple and the only thing of interest may be the deduction guide in case you're not familiar with C++17 deduction guides yet

  1. Or better yet, you can do a little twist with the same overloaded struct and make this possible:
something| match {
    [](A a) { std::cout << a.name << std::endl; },
    [](B b) { std::cout << b.type << std::endl; },
    [](C c) { std::cout << c.age << std::endl; }
};

I personally like the latter version for it's more streamlined and it resembles the match clause from some other languages.

The implementation is actually pretty straightforward, the only 2 changes are:

  • you rename overloaded to whatever you like, here it's match
  • you overload operator| for it to work
template <typename... Ts, typename... Fs>
constexpr decltype(auto) operator| (std::variant<Ts...> const& v, match<Fs...> const& match) {
    return std::visit(match, v);
}

I have this and a little more syntactic sugar (such as |is and |as "operator-lookalikes" for variant and any) in this repo :)

Alex Vask
  • 119
  • 8
1

A variation of @Silvio's answer is to create a template overload type that inherits from your functions:

#include <variant>
#include <iostream>

struct A {
    std::string name = "spencer";
};

struct B {
    std::string type = "person";
};

struct C {
    double age = 5;
};


template<typename...Func>
struct overload : Func... {
    using Func::operator()...;
};

template<typename...Func> overload(Func...) -> overload<Func...>;


int main()
{
    overload ovld {
        [](A a) { std::cout << a.name << std::endl; },
        [](B b) { std::cout << b.type << std::endl; },
        [](C c) { std::cout << c.age << std::endl; }
    };

    std::variant<A, B, C> something{B{}};

    std::visit(ovld, something);
}

Until C++17, the deduction guide is necessary for aggregate CTAD (Class Template Argument Deduction)

LWimsey
  • 6,189
  • 2
  • 25
  • 53