Search code examples
c++templatesvisitor-pattern

C++: Least painful way to write a Visitor to use in apply std::visit for a templated function?


I'm using a heavily templated library with a function that has multiple template arguments. I'd like to create a dynamic wrapper for this function by creating a class whose members will be the arguments to this templated function. I stumbled across std::visit, which requires a Visitor object, but it seems quite painful to write a visitor helper class to wrap the function, even when there are only a handful of possible template classes in consideration. Is there a less painful way to generate the Visitor?

Example with a manually implemented visitor helper:

#include <iostream>
#include <variant>

namespace NotMyLibrary
{
class A
{
    public:
    void execute()
    {
        std::cout << "From A!" << std::endl;
    }
};

class B
{
    public:
    void execute()
    {
        std::cout << "From B!" << std::endl;
    }
};

class C
{
    public:
    void apply()
    {
        std::cout << "From C!" << std::endl;
    }
};

class D
{
    public:
    void apply()
    {
        std::cout << "From D!" << std::endl;
    }
};

template<typename AorB, typename CorD>
void library_function_with_static_dependencies(AorB f, CorD g)
{
    size_t num_iterations = 1; //but really 1'000'000+
    for(size_t i=0; i < num_iterations; ++i)
    {
        f.execute();
        g.apply();
        std::cout << std::endl;
    }
}
} //end namespace NotMyLibrary


class VisitHelper
{
    public:
    void operator()(NotMyLibrary::A f, NotMyLibrary::C g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}
    void operator()(NotMyLibrary::A f, NotMyLibrary::D g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}   
    void operator()(NotMyLibrary::B f, NotMyLibrary::C g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}   
    void operator()(NotMyLibrary::B f, NotMyLibrary::D g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}   

};


class DynamicFunctionExecutor
{
    public:
    std::variant<NotMyLibrary::A, NotMyLibrary::B> f;
    std::variant<NotMyLibrary::C, NotMyLibrary::D> g;
    
    void dispatch()
    {
        VisitHelper vh{};
        std::visit(vh, f, g);
    }
};



int main()
{
    using namespace NotMyLibrary;
    DynamicFunctionExecutor e;
    e.f = A();
    e.g = C();
    e.dispatch();
    
    e.f = B();
    e.g = D();
    e.dispatch();
    
}

Solution

  • You can make operator() a template:

    
    class VisitHelper
    {
        public:
        /* Replace:
        void operator()(NotMyLibrary::A f, NotMyLibrary::C g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}
        void operator()(NotMyLibrary::A f, NotMyLibrary::D g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}   
        void operator()(NotMyLibrary::B f, NotMyLibrary::C g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}   
        void operator()(NotMyLibrary::B f, NotMyLibrary::D g) {NotMyLibrary::library_function_with_static_dependencies(f,g);}   
         */
        template<typename AorB, typename CorD>
        void operator()(AorB f, CorD g) {
            NotMyLibrary::library_function_with_static_dependencies(f,g);
        }
    };
    
    // ...
    
        void dispatch()
        {
            std::visit(VisitHelper{}, f, g);
        }
    

    Which you can further replace with a generic lambda:

        void dispatch()
        {
            std::visit([](auto f, auto g) {
                NotMyLibrary::library_function_with_static_dependencies(f, g);
            }, f, g);
        }
    

    ... Which is the same as creating an object of a class type with a templated operator(), like VisitHelper, and passing it to std::visit{}.

    Most of the time you use std::visit you should use a generic lambda. Usually, you want const auto& to prevent copies.