Hurricane Development
Hurricane Development

Reputation: 2464

C++ Variant visit overloaded function

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

Answers (3)

Alex Vaskov
Alex Vaskov

Reputation: 248

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 :)

Upvotes: 8

LWimsey
LWimsey

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

Silvio Mayolo
Silvio Mayolo

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

Related Questions