C++ Variant visit overloaded function
Asked Answered
A

3

9

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?

Applicant answered 6/4, 2021 at 0:48 Comment(2)
There's been proposals kicking around for this. wg21.link/p1170 is the latest AFAIK.Aguila
Does this answer your question? Is there a clean way to turn an overload set into a visitor suitable for use with std::visit?Aguila
M
12
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.

Mizzenmast answered 6/4, 2021 at 0:59 Comment(0)
H
6

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

Harilda answered 3/6, 2022 at 14:36 Comment(0)
M
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)

Melisent answered 8/3, 2022 at 12:31 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.