Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Storing and calling functions of different arguments in one function container

Tags:

c++

Here is a sample design code of what I want to achieve. Basically I wanna store handler functions for different handlerNames and these handler functions can be of variable arguments.

The handler functions should be called on events with the required arguments are passed with Script::Handle(...)

How can I achieve this? Maybe its possible with Variadic Templates?

class Script
{
public:
    Script() { /* ... */ }

    template<typename TFunction>
    void AddHandler(const char *handlerName, TFunction &&function)
    {
        _handlerMap[handlerName] = std::move(function);
    }

    void Handle(const char *handlerName, ...)
    {
        _handlerMap[handlerName](...);
    }

private:
    typedef std::map<std::string, std::function<void()>> HandlerMapType;
    HandlerMapType _handlerMap;
};

//Handler functions
handlerOne() { std::cerr << "One"; }
handlerTwo(std::string a1, int a2) { std::cerr << "Two"; }
handlerThree(bool a1) { std::cerr << "Three"; }

int main(int argc, char **argv)
{
    Script script;
    script.AddHandler("One", std::bind(&handlerOne));
    script.AddHandler("Two", std::bind(&handlerTwo));
    script.AddHandler("Three", std::bind(&handlerThree));

    script.Handle("One");
    script.Handle("Two, "string", 96);
    script.Handle("Three", true);

    script.Handle("Three", "what should happen here?"); //String passed instead of bool
}
like image 976
Saif Ur Rehman Avatar asked Jun 25 '15 00:06

Saif Ur Rehman


1 Answers

Let me prefix by saying that this is not a trivial thing to do in C++. And I will go as far to say that you should consider whether this is really something you need in your use case. In your example, you are asking for genericism that you can't really use. You will in any case need to know the signature of the function you are calling to call it properly; in that case what purpose is served by putting them in a container?

Generally, you'd do something like this if you are writing a middle layer of code. In your example, this would be equivalent to writing code that enables another user to call Handle. A common concrete example of this is to write a factory where objects in the factory may be instantiated using different arguments. However, it can't really be "different" arguments, at least not without some crazy casting. The solution is to make all the functions take the same argument, but make the argument a dynamic type that can store whatever arguments you want:

using argument_type = std::unordered_map<std::string, boost::any>;

void print(const argument_type & arg) {
  auto to_print = boost::any_cast<std::string>(arg["to_print"]);
  std::cerr << to_print << std::endl;
}

void print_none(const argument_type & arg) {
      std::cerr << "none" << std::endl;
}

using my_func_t = std::function<void(const argument_type &)>;

std::vector<my_func_t> v;
v.emplace_back(print);
v.emplace_back(print_none);

// create some argument_types, feed them to f.

The above is not code that has been tested, nor with a working main, but I think this should give you a sense of how you could accomplish what you want.

edit: I thought about it a bit more, and I decided to elaborate a bit more on the "crazy casting" way. I suppose it's not really more crazy, but I strongly prefer what I showed above. The alternative is to completely type erase the functions themselves, and pass the arguments using a variadic template.

void print(std::string to_print) {
  std::cerr << to_print << std::endl;
}

void print_none() {
      std::cerr << "none" << std::endl;
}


std::vector<boost::any> v;
v.emplace_back(std::function<void(std::string)>(print));
v.emplace_back(std::function<void(void)>(print_none));

template <typename ... Args>
void call(const std::vector & funcs, int index, Args... args) {
  auto f = boost::any_cast<std::function<void(Args...)>>(funcs[index]);
  f(std::forward<Args>(args)...);
}

// unsure if this will actually work
call(f, 0, std::string("hello"));

The code above is very fragile though, because the types you pass to call will be deduced against, and then the cast will try to cast to a std::function that matches that signature. That exact signature. I don't have a lot of confidence that this will work out; if it's a reference, vs value, vs rvalue, etc. Casting back to a different std::function than what you put in is undefined behavior.

In summary, I'd either try to avoid needing to do this entirely, or go with the first solution. It's much less fragile, and it's better to be upfront about the fact that you are erasing the signatures of these functions.

like image 104
Nir Friedman Avatar answered Oct 31 '22 05:10

Nir Friedman