Matteo Monti
Matteo Monti

Reputation: 8930

Wrapper for template callback?

Immediate callback

Consider the following example:

template <typename lambda> void call_me_back(const lambda & callback)
{
    // Very complicated calculation to find the meaning of everything

    callback(42);
}

int main()
{
    call_me_back([](const int & value)
    {
        std :: cout << value << std :: endl;
    });
}

Here I provide call_me_back with a lambda that accepts an int. call_me_back calls callback after some long calculation, and callback prints it out. Easy as cake.

Now, say that depending on the execution, call_me_back needs to call callback either with an int or with some other type. We can edit the previous example to

template <typename lambda> void call_me_back(const lambda & callback)
{
    // Very complicated calculation to find the meaning of everything

    if(rand() % 2)
        callback(42);
    else
        callback("Kill all the humans.");
}

int main()
{
    call_me_back([](const auto & value)
    {
        std :: cout << value << std :: endl;
    });
}

So far so good. Now callback can do all sorts of tricks and handle value depending on its type.

Delayed callback

Now, back to the first example. Say that call_me_back is not ready to immediately call callback with an int. What it can do, however, is to store the callback somewhere, and then call it later.

For example:

std :: function <void(const int &)> the_callback;

template <typename lambda> void call_me_back(const lambda & callback)
{
    the_callback = callback;
}

void ready_to_answer()
{
    the_callback(42);
}

int main()
{
    call_me_back([](const auto & value)
    {
        std :: cout << value << std :: endl;
    });
}

Now, instead of immediately calling callback, call_me_back stores callback in an std :: function <void(const int &)> object (I know, it's in the global scope, bear with me). Many things can then happen and at some point someone can call ready_to_answer, which retrieves the previously stored callback and calls it. For example, ready_to_answer can be called by another thread, but there is a whole lot of reasons why one would need a paradigm like this, where the callback can be stored and called later.

The problem

What if I wanted to implement the second example, but with a delayed callback?. I don't seem to be able to get my head around this.

I can imagine that std :: function is implemented with a virtual call operator that accepts some specific type. The std :: function then wraps a pointer / reference to a template wrapper class that stores the actual lambda and implements the call operator by forwarding its argument to the lambda it's storing. Nice and easy. But I can't have template virtual methods!

I have tried coming up with all sorts of solutions, but I couldn't find anything that could reasonably work. Is this really impossible to do? Is it impossible to have some externally provided lambda that accepts a const auto & argument stored somewhere, and then call it later?

Upvotes: 2

Views: 88

Answers (3)

Passer By
Passer By

Reputation: 21131

You could implement a class template that stores the lambda for you.

#include<iostream>

template<typename L>
struct Telephone
{
    L l;

    Telephone(L l) : l{std::move(l)} {}

    template<typename... Args>
    decltype(auto) ready_to_answer(Args&&... args)
    {
        return l(std::forward<Args>(args)...);   
    }
};

template<typename L>
auto call_me_back(L&& l)
{
    return Telephone<std::decay_t<L>>(std::forward<L>(l));
}

int main()
{
    auto telephone = call_me_back([](auto x){ std::cout << x << std::endl; });
    telephone.ready_to_answer(42);
    telephone.ready_to_answer("Kill all the humans.");
}

This has the downside that Telephone is templated on a lambda and will be a different type for each lambda.

If you know beforehand what the function signatures looks like, you could have Telephone inherit from a common base class and have virtual non-template methods.

Slap on some automatic construction and management of Telephone and you basically implemented another std::function but with custom signatures

struct Phone
{
    // all Telephone<L> inherits from Phone and have these methods

    virtual void ready_to_answer(int) = 0;
    virtual void ready_to_answer(const char*) = 0;
};

struct Spouse
{
    std::unique_ptr<Phone> phone;

    template<typename L>
    Spouse(L&& l) : phone{ new Telephone<std::decay_t<L>>{std::forward<L>(l)} } {}

    void ready_to_answer(int i) { phone->ready_to_answer(i); }
    void ready_to_answer(const char* str) { phone->ready_to_answer(str); }
};

EDIT: I went ahead and implemented a buffed up version of std::function which accepts any number of signatures, so you could write

function<void (int), void (std::string)> callback =
    [](auto x){std::cout << x << std::endl;};

callback(42);
callback("Kill all humans!");

Live

Suffice to say the implementation is not trivial and there is just a bit too much to explain here.

Upvotes: 1

yuri kilochek
yuri kilochek

Reputation: 13486

You are correct, it is impossible for an unbounded set of types, but you can do it if you know all the types in advance:

std :: function <void(const std :: variant<int, std :: string> &)> the_callback;

template <typename lambda> void call_me_back(const lambda & callback)
{
    the_callback = [callback](const auto & arg)
    {
        std :: visit(callback, arg);
    };
}

template <typename T> void ready_to_answer(const T & x)
{
    the_callback(x);
}

int main()
{
    call_me_back([](const auto & value)
    {
        std :: cout << value << std :: endl;
    });

    if (std :: rand() % 2)
    {
        ready_to_answer(42);
    }
    else
    {
        ready_to_answer("Hi!");
    }
}

Upvotes: 7

user7860670
user7860670

Reputation: 37478

In your second example you don't pass a function, you pass a reference to anonymous lambda object with two methods operator()(int) and operator(char const (&)[..]). So to store this kind of callback you need to either copy lambda object or store particular callback methods, that is using more than one ::std::function with corresponding signatures. Actually explicitly passing two callbacks for both cases would be clearer.

std::function< void (int) > delalyed_int_cb;
std::function< void (const char *) > delalyed_str_cb;

template< typename callable_taking_int, typename callable_taking_string > void
call_me_back(callable_taking_int && int_cb, callable_taking_string && str_cb)
{
    delalyed_int_cb = std::forward< callable_taking_int >(int_cb);
    delalyed_str_cb = std::forward< callable_taking_str >(str_cb);
    ...
}

void
ready_to_answer()
{
    if(rand() % 2)
    {
        delalyed_int_cb(42);
    }
    else
    {
        delalyed_str_cb("Kill all the humans.");
    }
}

Upvotes: 1

Related Questions