Vector of std::function with different signatures

2019-01-17 22:42发布

问题:

I have a number of callback functions with different signatures. Ideally, I would like to put these in a vector and call the appropriate one depending on certain conditions.

e.g.

void func1(const std::string& value);

void func2(const std::string& value, int min, int max);

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    func2,
};

I realise the above isn't possible, but I wonder if there are any alternatives I should consider. I haven't been able to find any yet, and I've experimented with std::bind but not managed to achieve what I want.

Is such a thing possible?

回答1:

You haven't said what you expect to be able to do with func2 after putting it in a vector with the wrong type.

You can easily use std::bind to put it in the vector if you know the arguments ahead of time:

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    std::bind(func2, std::placeholders::_1, 5, 6)
};

Now functions[1]("foo") will call func2("foo", 5, 6), and will pass 5 and 6 to func2 every time.

Here's the same thing using a lambda instead of std::bind

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    [=](const std::string& s){ func2(s, func2_arg1, func2_arg2); }
};

If you don't know the arguments yet, you can bind references to some variables:

int func2_arg1 = 5;
int func2_arg2 = 6;
const std::vector<std::function<void(std::string)>> functions
{
    func1,
    std::bind(func2, std::placeholders::_1, std::ref(func2_arg1), std::ref(func2_arg2))
};

Now functions[1]("foo") will call func2("foo", func2_arg1, func2_arg2), and you can assign new values to the integers to pass different arguments to func2.

And using a lambda function instead of std::bind

const std::vector<std::function<void(std::string)>> functions
{
    func1,
    [&](const std::string& s){ func2(s, func2_arg1, func2_arg2); }
};

This is pretty ugly though, as you need to keep the int variables around for as long as the callable object (the closure or the bind expression) referring to them exists.



回答2:

What you want is possible through polymorphism. The idea is to create a class with a specific signature, which at runtime will call different methods. For example:

#include <iostream>
#include <functional>
#include <memory>
#include <vector>

void foo(int) {
    std::cout << "I'm foo!\n";
}

int bar(char, double) {
    std::cout << "I'm bar!\n";
}

class MyFunction {
    public:
        virtual ~MyFunction(){}

        virtual void operator()() = 0;
};

class MyFunctionA : public MyFunction {
    public:
        virtual void operator()() {
            foo(4);
        }
};

class MyFunctionB : public MyFunction {
    public:
        MyFunctionB(std::function<int(char,double)> f, char arg1, double arg2) : fun_(f), arg1_(arg1), arg2_(arg2) {} 

        virtual void operator()() {
            fun_(arg1_, arg2_);
        }
    private:
        std::function<int(char,double)> fun_;
        char arg1_;
        double arg2_;
};

int main() {
    using MyFunPtr = std::unique_ptr<MyFunction>;
    std::vector<MyFunPtr> v;

    v.emplace_back(new MyFunctionA());
    v.emplace_back(new MyFunctionB(bar, 'c', 3.4));

    for ( auto&& myfun : v ) {
        (*myfun)();
    }
    return 0;
}

You can make the derived classes as complicated as you need be, but since in the end they all have the same interface you will be able to call all of them.



回答3:

Direct answer to your question is "NO". Any runtime container would only let you store objects of the same type and std::function<> instantiated with different signatures will be different data types.

Generally the reason you may want to have "a vector of functions with different signatures" is when you have something like the below (three step processing where input interface is unified (buffer& buf and output interface is unified on_event(Event evt)), but the layer in the middle is heterogeneous process_...(...)

receive_message(buffer& buf)
  switch(msg_type(buf))
    case A: 
    case B:
    ...

process_A(A& a, One x, Two y)
  ...
  dispatch(Event evt);
  ...

process_B(B& b, Three x);
  ...
  dispatch(Event evt);
  ...

In a solution not involving metaprogramming you'd typically pre-cook a functor doing the end-to-end at initialization time and store those in the vector:

vector <std::function<void(buffer& buf)>> handlers;


回答4:

Not sure how useful this would be for you, it is based on boost::any, redundant parameters are ignored. You can add try...catch for boost::bad_any_cast to prevent crash in case of mismatch between arguments' and parameters' types. Though I think regular std::bind is a better choice.

DEMO

#include <boost/any.hpp>
#include <functional>
#include <vector>
#include <cstddef>
#include <memory>
#include <tuple>
#include <utility>
#include <iostream>
#include <string>

struct IGenericFunction
{
    virtual ~IGenericFunction() = default;

    virtual void call(boost::any a1 = boost::any{}
                    , boost::any a2 = boost::any{}
                    , boost::any a3 = boost::any{}
                    , boost::any a4 = boost::any{}) = 0;
};

template <typename... Args>
class GenericFunction : public IGenericFunction
{
public:
    GenericFunction(std::function<void(Args...)> f) : _f{ f } {}

    virtual void call(boost::any a1 = boost::any{}
                    , boost::any a2 = boost::any{}
                    , boost::any a3 = boost::any{}
                    , boost::any a4 = boost::any{}) override
    {
        call_func(std::make_tuple(a1, a2, a3, a4)
                , std::make_index_sequence<sizeof...(Args)>{});
    }

private:            
    template <typename Tuple, std::size_t... Indices>
    void call_func(Tuple t, std::index_sequence<Indices...> s)
    {
        _f(boost::any_cast<
                typename std::tuple_element<Indices, Params>::type
           >(std::get<Indices>(t))...);
    }

    std::function<void(Args...)> _f;

    using Params = std::tuple<Args...>;
};

template <typename... Args>
std::shared_ptr<IGenericFunction> make_generic_function_ptr(void(*f)(Args...))
{
    return std::make_shared<GenericFunction<Args...>>(f);
}

void func1(const std::string& value)
{
    std::cout << "func1 " << value << std::endl;
}

void func2(const std::string& value, int min, int max)
{
    std::cout << "func2 " << value << " " << min << " " << max << std::endl;
}

int main()
{
    std::vector<std::shared_ptr<IGenericFunction>> functions;

    functions.push_back(make_generic_function_ptr(&func1));    
    functions.push_back(make_generic_function_ptr(&func2));

    for (auto f : functions)
    {
        f->call(std::string("abc"), 1, 2);
    }
}


回答5:

If you've got an int and a string, you cannot put them in one vector but you can put them in one struct or std::tuple<>. The same applies for two function types.



回答6:

std::function erases the exact type of the function object but preserves the function call signature. If you cannot bind the extra arguments in advance as Jonathan Wakely recommends, you can use a boost::variant< std::function<...>, std::function<...> > as your vector member. At the call site you can then check if the vector contains the right kind of function object and call it accordingly.



回答7:

As JBL mentioned: how would you call them, if you don't know their signatures?

Think about turning your min, max arguments into a parameter type with some base class Parameter, the callback signature will be void(const std::string&, const Parameter&) or void(const std::string&, const Parameter*) in case you wish nullptr to indicate no additional parameters. Now your callbacks will need to check if they were given the right parameters if any. That may be done by using a visitor, typeid or an enum. There's pros and cons to all of those.

How will you decide on which callback to call? I think you should turn your C-style callbacks into handler objects, they might implement a function bool canHandle(const Parameter&) to test if the handler is applicable to the parameters presented.

Jonathan Wakely and Svalorzen present their approach where the parameters and the function are one and the same object (1-to-1 relationship). In this example they are separate (in case you have multiple-to-multiple relationships):

#include <cassert>
#include <string>
#include <typeinfo>
#include <vector>

class ParameterBase {
public:
    ParameterBase(const std::string& value) : m_value(value) { }
    virtual ~ParameterBase() { }
    const std::string& GetValue() const { return m_value; }
private:
    std::string m_value;
};

class HandlerBase {
public:
    virtual bool CanHandle(const ParameterBase& params) const = 0;
    virtual void Handle(const ParameterBase& params) = 0;
};

class Handler1 : public HandlerBase {
public:
     class Parameter : public ParameterBase {
     public:
          Parameter(const std::string& value) : ParameterBase(value) { }
          ~Parameter() { }
     };

     bool CanHandle(const ParameterBase& params) const { return typeid(Parameter) == typeid(params); }
     void Handle(const ParameterBase& params) {
          assert(CanHandle(params));
          const Parameter& p = static_cast<const Parameter&>(params);
          // implement callback1
     }
};

void foo(const std::vector<HandlerBase*>& handlers) {
     Handler1::Parameter params("value");
     for(auto handler : handlers)
         if(handler->CanHandle(params)) {
             handler->Handle(params);
             // no break: all handlers may handle params
             // with break: only first handler (if any) handles params
         }
}