Reputation: 2464
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?
Upvotes: 9
Views: 9432
Reputation: 248
Is there a simpler way? Well, there might be a syntactically more streamlined way:
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
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:
overloaded
to whatever you like, here it's match
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 :)
Upvotes: 8
Reputation: 6677
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)
Upvotes: 1
Reputation: 70297
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.
Upvotes: 12