Search code examples
c++variant

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?


Solution

  • 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.