Tuesday, April 23, 2013

Making functions: How to provide a simple interface when working with std::function

While working on the lundi project, a minor, but not negligible, issue arose: How can we spare the user from producing manually all those std::functions from their callable objects?

The hypothetical expose function template takes std::function<R(P...)> and exposes them to the lua VM. A typical use case would be:

std::function<void(int, int)> lamarck = [](int a, int b){ 
  std::cout << a << b; 
};
expose(lamarck);

That's nice and all, but why do you need to write the signature of the function you just typed? That's redundant. The thing is, lambdas are actually function objects. The preceding lambda declaration is roughly equivalent to

struct blanche {
  void operator()(int a, int b) const { 
    std::cout << a << b; 
  }
};
std::function<void(int, int)> lamarck = blanche();

Note the braces; we're not assigning a type here.

Now, we could put pretty much anything in function objects; the only thing that makes them function objects are the presence of operator(). That would be insane, of course, to provide an implicit conversion to std::function.

Nobody is keeping you from doing it, though:

template<typename R, typename... P>
std::function<R(P...)> make_function(R(func const &)(P...)) {
  return std::function<R(P...)>(func);
}

Well, that's the easy part, but what about function objects? You can't just match a function object's call signature because it's not easily accessible. You can, though, look at his operator() to work out the parameter and return types. Note that it's not possible if operator() is a template or if it's overloaded.

This is where decltype is useful:

typedef decltype(&T::operator()) ptmf_type;
ptmf_type should look (in the case of void(int, int)) like void (mytype::*)(int, int) const, which can be matched in a template to extract all the needed types. We might as well work out directly the function type:
template<typename T>
struct ptmf_traits {
  typedef void type;
};

template<typename O, typename R, typename... P>
struct ptmf_traits<R(O::*)(P...) const> {
  typedef std::function<R(P...)> type;
};

Now, you can just use ptmf_traits<decltype(&LambdaType::operator())>::type to get a function type:

template<typename Func>
ptmf_traits<decltype(&Func::operator())>::type 
make_function_from_lambda(Func const &func) {
  return func;
}

With this function, the type of the std::function produced is fully deduced from the argument type:

auto my_func = make_function_from_lambda([](int a, int b){
  std::cout << a << b;
});
// my_func's type is std::function<void(int, int)>

Of course, you need a bit of boilerplate to accept free functions and maybe some tweaking to make it do what you want, but the core idea is there. You can find a more complete version here.

You may want to follow me on twitter.

7 comments:

  1. what's wrong about

    auto lamarck = [](int a, int b){
    std::cout << a << b;
    };
    register(lamarck);

    ReplyDelete
  2. Márton, the issue with capturing the type directly is that you end up with some opaque class type (probably like main()::__lambda0) that isn't helpful when it comes to inspecting the types of the callable's parameters. If you want to do so, you will have to use this technique sooner or later -- std::function is, in this case, only used as a convenience container because it's easy to match against using templates such as template<class R, class... P> void somefunc(std::function<R(P...)> const &func).

    ReplyDelete
  3. I know that, but doesn't register take a std::function? Then the opaque type would be converted to it, as std::funciton has a non-explicit constructor taking any type.

    TL;DR The whole problem is a non-issue if you define register as

    void register(std::function f);

    ReplyDelete
  4. No, you can't implicitly convert a callable to std::function. Try it for yourself!

    ReplyDelete
  5. Just to clarify: Of course you can convert implicitly if you define the function signature beforehand. What you can't do is pass a callable to a function template accepting a std::function<T>.

    ReplyDelete