Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to pass generic lambda as non-template argument

I have a toy example that I'd like to modify architecturally to remove type dependency of Processor on EmitterT:

#include <iostream>
#include <utility>

using namespace std;

struct Emitter {
    void e(int) { cout << "emitting int\n";}
    void e(double) { cout << "emitting double\n";}
    void e(char*) { cout << "emitting char*\n";}
    void e(const char*) { cout << "emitting const char*\n";}
};

template <typename EmitterT>
struct Processor {

    Processor(EmitterT e) : e_{e} {}

    template <typename T>
    void process(T&& value) {
        cout << "some processing... ";
        e_(std::forward<T>(value));
    }

    EmitterT e_;

};

template<typename Emitter_>
Processor<Emitter_> makeProcessor(Emitter_ e) { return Processor<Emitter_>(e);}

int main() {
    Emitter em;
    auto p = makeProcessor([&em](auto v){em.e(v);});


    p.process(1);
    p.process("lol");
    return 0;
}

Motivation

I'd like to decouple part responsible for utilizing results of processing from the processing itself. The Emitter class structure is given to me, so I have to support overloaded functions.

I'd like to pass a lambda function to a processor that will use it. Kind of like a callback mechanism, however it must be a generic lambda, to support overloads.

What I've tried:

The example I wrote works, but it depends on Emitter type as a template parameter. I don't like Processor type to change based on Emitter. It's also contagious, I have a real Processor hierarchy and Emitter spread like const or worse.

After reading https://stackoverflow.com/a/17233649/1133179 I've tried playing with below struct as a member:

struct EmitterC {
    template<typename T>
    void operator()(T value) { }
};

But I cannot figure out a way to defer implementation of Emitter after Processor when using it as a normal parameter. It worked out with a forward declaration and a reference EmitterC& but it supports one only Emitter definition. The only way I could come up with was to drop lambda, and make virtual overloads in EmitterC for every type I expect in Emitter and use it as a base class.

So, Is there a way to pass the (generic) lambda as a parameter, so that Processor type doesn't depend on Emitter?

I am restricted to c++14, but I am interested in more modern standards as well if the have better support.

like image 715
luk32 Avatar asked Jul 22 '19 15:07

luk32


3 Answers

If you are willing to pay a high runtime cost in exchange for minimal constraints, you can use std::function with std::any (for C++14, use boost::any):

#include <iostream>
#include <utility>
#include <any>
#include <functional>

struct Processor {
    Processor(std::function<void(std::any)> e) : e_{e} {}

    template <typename T>
    void process(T&& value) {
        std::cout << "some processing... ";
        e_(std::forward<T>(value));
    }

    std::function<void(std::any)> e_;
};

struct Emitter {
    void e(int) { std::cout << "emitting int\n";}
    void e(double) { std::cout << "emitting double\n";}
    void e(char*) { std::cout << "emitting char*\n";}
    void e(const char*) { std::cout << "emitting const char*\n";}
};

int main() {
    Emitter em;
    auto p = Processor(
        [&em](std::any any){
            // This if-else chain isn't that cheap, but it's about the best
            // we can do. Alternatives include:
            // - Hashmap from `std::type_index` (possibly using a perfect hash)
            //   to a function pointer that implements this.
            // - Custom `any` implementation which allows "visitation":
            //
            //   any.visit<int, double, char*, char const*>([&em] (auto it) {
            //        em.e(it);
            //   });
            if (auto* i = std::any_cast<int>(&any)) {
                em.e(*i);
            } else if (auto* d = std::any_cast<double>(&any)) {
                em.e(*d);
            } else if (auto* cstr = std::any_cast<char*>(&any)) {
                em.e(*cstr);
            } else {
                em.e(std::any_cast<char const*>(any));
            }
        }
    );


    p.process(1);
    p.process("lol");
    return 0;
}

std::any and std::function are both owning type erased wrappers. You may have heap allocations for this, or you might fit inside their small object optimization. You will have virtual function calls (or equivalent).

Compiler Explorer link

like image 144
Justin Avatar answered Nov 04 '22 15:11

Justin


IMHO: Inheritance is here for that.

#include <iostream>
#include <utility>

using namespace std;

struct BaseEmitter {
    virtual void e(int) =0;
    virtual void e(double)=0;
    virtual void e(char*)=0;
    virtual void e(const char*)=0;
};

struct Emitter :public BaseEmitter {
    virtual void e(int) { cout << "emitting int\n";}
    virtual void e(double) { cout << "emitting double\n";}
    virtual void e(char*) { cout << "emitting char*\n";}
    virtual void e(const char*) { cout << "emitting const char*\n";}
};

struct Processor {
    BaseEmitter& e_;
    Processor(BaseEmitter& e) : e_(e) {}

    template <typename T>
    void process(T&& value) {
        cout << "some processing... ";
        e_(std::forward<T>(value));
    }
};


int main() {
    Emitter em;
    auto p = Processor(em);
    p.process(1);
    p.process("lol");
    return 0;
}

You can do a mix in order to capture the lambda, just by inheritance in the interface:

struct bypass
{
        virtual void operator()() = 0;
};

template<typename callable> struct capture: public bypass
{
        callable& _ref;
        capture(callable &ref)
        : _ref(ref)
        {;};

        virtual void operator()()
        {
                _ref();
        }
};

struct test
{
        bypass *_c;
        template<class T> test(T& callback)
        : _c(nullptr)
        {
          _c = new capture<decltype(callback)>(callback);
        };
        virtual ~test()
        {
            delete _c;
        };
        void doit()
        {
            (*_c)();
        }

};



int main(int argc, char* argv[])
{
        auto lambda = [](){std::cout << "hello\n";};
        test z=test(lambda);
        z.doit();
        return 0;
}
like image 26
Mel Viso Martinez Avatar answered Nov 04 '22 15:11

Mel Viso Martinez


Is it possible to pass generic lambda as non-template argument

It is not possible to declare a non-template function that accepts a lambda as an argument. The type of a lambda is anonymous: It has no name. It is not possible to write a function declaration that accepts an argument of an anonymous type.

The type of the lambda can be deduced, which is why lambdas can be passed into function templates whose argument types are deduced.

While this answers the question, it does not offer a solution. I don't think a solution is going to be simple.

like image 2
eerorika Avatar answered Nov 04 '22 15:11

eerorika