0

I maintain an open-source lockless threading library designed for high speed parallel loop unrolling which is used in a couple commercial video games. It has incredibly low-overhead, around 8 clocks for creating a message and around 500 (per thread, including latency) for the entire dispatch and remote execute overhead. I say this first to explain why I don't simply use use std::function and bind.

The library packages a function call and the arguments to the function call in a message (of type Functor<>). Then calls it remotely with copies of the arguments.

I have recently rewritten the library to package remote calls using STL style template meta programming rather than the dated C style macros it originally used. Amazingly this only increased the overhead by two ticks but I cannot figure out how to create a packaging function that takes both lambdas and function pointers.

Desired Usage:

CreateFunctor([](int a, int b){doSomething(a, b)}, 1, 2);

or

CreateFunctor(&doSomething, 1, 2);

Currently I have to divide these cases into two separate functions, (CreateFunctor and CreateFunctorLambda). If I could combine them I would be able to merge my packaging and dispatching stages into one neat function call and simplify the API.

The problem is that the code for deducing the arguments of a lambda cannot seem to share a template override with the code for deducing the arguments of a function. I've tried using enable_if and it still executes the ::* portion of the lambda version and causes compiler errors with function pointers.

Relevant Snippet:

template <typename... Arguments>
inline Functor<Arguments...> CreateFunctor(void(*func)(Arguments...))
{
    return Functor<Arguments...>(func);
};

template <typename... Arguments>
inline Functor<Arguments...> CreateFunctor(void(*func)(Arguments...), Arguments ... arg)
{
    Functor<Arguments...> ret(func);
    ret.Set(arg...);
    return ret;
};

// template to grab function type that lambda can be cast to
// from http://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda
template <class T>
struct deduce_lambda_arguments
    : public deduce_lambda_arguments<typename std::enable_if<std::is_class<T>::value, decltype(&T::operator())>::type>
{};

template <class ClassType, typename... Args>
struct deduce_lambda_arguments<void(ClassType::*)(Args...) const>
    // we specialize for pointers to member function
{
    typedef void(*pointer_cast_type)(Args...);
    typedef Functor<Args...> functor_type;
};

template <typename F, typename... Args>
inline auto CreateFunctorLambda(F f, Args... arg) -> typename deduce_lambda_arguments<F>::functor_type
{
    deduce_lambda_arguments<F>::functor_type ret((deduce_lambda_arguments<F>::pointer_cast_type) f);
    ret.Set(arg...);
    return ret;
};

template <typename F>
inline auto CreateFunctorLambda(F f) -> typename deduce_lambda_arguments<F>::functor_type
{
    return deduce_lambda_arguments<F>::functor_type((deduce_lambda_arguments<F>::pointer_cast_type) f);
};
George Davison
  • 103
  • 1
  • 9
  • If I'm reading this correctly, you only accept captureless non-generic lambdas, is that correct? – T.C. Dec 09 '15 at 18:12
  • Correct. They are all converted into a struct containing a __cdecl function pointer, a list of arguments and a size under the hood. Captures would make this much more complicated, and would not be safe for deferred execution anyway. Once the arguments are known the lambda can be cast to the __cdecl pointer and stored with one mov. – George Davison Dec 09 '15 at 18:19
  • Have you measured the performance of std::function? It has optimisations for when the function object is "small". I'd be surprised if you can outperform it. – Richard Hodges Mar 31 '16 at 08:01
  • Richard, Yes I have evaluated the speed of std::function (which would require bind in this case, very non-lightweight). Using std::function even for very small or empty enclosures with void arguments doubles the overhead time from around 150 microseconds per thread job start to around 400. And that includes queueng and interthread communication. For most applications that would be fine but for larger functions it becomes 2 orders of magnitude more expensive due to the memory management. – George Davison Sep 28 '16 at 21:28
  • Can you share a link to your library? – karliwson Nov 26 '17 at 17:21

1 Answers1

1

The trick is to ensure that you never evaluate &T::operator() for things-that-you-want-to-support-but-are-not-lambdas. One way to do this is adding an extra template parameter and specializing on that:

template <class T, bool = std::is_class<T>::value>
struct compute_functor_type
    : public compute_functor_type<decltype(&T::operator())>
{};

template <class ClassType, typename... Args>
struct compute_functor_type<void(ClassType::*)(Args...) const, false>
{
    typedef void(*pointer_cast_type)(Args...);
    typedef Functor<Args...> functor_type;
};

template <class ClassType, typename... Args>
struct compute_functor_type<void(ClassType::*)(Args...), false>
{
    typedef void(*pointer_cast_type)(Args...);
    typedef Functor<Args...> functor_type;
};

template <typename... Args>
struct compute_functor_type<void(*)(Args...), false>
{
    typedef void(*pointer_cast_type)(Args...);
    typedef Functor<Args...> functor_type;
};


template <typename F, typename... Args>
inline auto CreateFunctor(F f, Args... arg) 
         -> typename compute_functor_type<F>::functor_type
{
    typename compute_functor_type<F>::functor_type ret((typename compute_functor_type<F>::pointer_cast_type) f);
    ret.Set(arg...);
    return ret;
};

template <typename F>
inline auto CreateFunctor(F f) -> typename compute_functor_type<F>::functor_type
{
    return typename compute_functor_type<F>::functor_type((typename compute_functor_type<F>::pointer_cast_type) f);
};

Saving the (way shorter) original approach here for people who don't have to use MSVC:

Since you only care about captureless non-generic lambdas and convert them to function pointers anyway, this is easy.

Create a matching Functor type from a function pointer:

template<class... Args>
Functor<Args...> make_functor(void (*f)(Args...)) { return {f}; }

And force conversion to function pointer via the unary + operator:

template <class F>
inline auto CreateFunctor(F f) -> decltype(make_functor(+f))
{
    return make_functor(+f);
}

template <class F, typename... Arguments>
inline auto CreateFunctor(F f, Arguments ... arg) -> decltype(make_functor(+f))
{
    auto ret = make_functor(+f);
    ret.Set(arg...);
    return ret;
}
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • This fails to compile when lambdas are passed. It cannot deduce the arguments of the lambda. 1>ParLLTest.cpp(53): error C2672: 'ParLL::CreateFunctor': no matching overloaded function found 1>ParLLTest.cpp(53): error C2893: Failed to specialize function template 'unknown-type ParLL::CreateFunctor(F,Arguments...)' 1> ParLLTest.cpp(53): note: With the following template arguments: 1> ParLLTest.cpp(53): note: 'F=main::' 1> ParLLTest.cpp(53): note: 'Arguments={}' – George Davison Dec 09 '15 at 18:39
  • @GeorgeDavison [Works for me with GCC and clang](http://coliru.stacked-crooked.com/a/493061718cf57403). I wonder what MSVC is hallucinating here... Edit: Great, nonstandard calling conventions. /sigh – T.C. Dec 09 '15 at 21:08
  • Did you mean MSVC uses nonstandard calling convention or Clang and GCC? – George Davison Dec 09 '15 at 21:37
  • @GeorgeDavison MSVC. Its lambdas have four separate conversion functions to function pointers. – T.C. Dec 09 '15 at 21:39
  • Yeah, I noticed that from the warning messages in earlier attempts to fix this. It's a chicken and egg problem, i can't get the right conversion operator without knowing the arguments. Then I found the example I used for deduce_lambda_arguments in another SO question. Maybe If I knew why that worked I could think of a more general purpose way of writing it. – George Davison Dec 09 '15 at 21:43
  • @GeorgeDavison Oh, I know how to do it, was just too lazy. – T.C. Dec 09 '15 at 21:48
  • New version works great thanks! And I think the first version will be helpful to a lot of people too. Only one caveat, the template now converts arguments passed as untyped NULL to ints in the signature of CreateFunctor (but not Functor<>). But we can live with explicit typing, or force a cast in the template. – George Davison Dec 09 '15 at 22:24