Template to convert any lambda function (including capturing lambdas) to a std::function object
Asked Answered
D

1

4

I have the following code that can convert a lambda into a C-style function pointer. This works for all lambdas including lambdas with captures.

#include <iostream>
#include <type_traits>
#include <utility>

template <typename Lambda>
struct lambda_traits : lambda_traits<decltype(&Lambda::operator())>
{};

template <typename Lambda, typename Return, typename... Args>
struct lambda_traits<Return(Lambda::*)(Args...)> : lambda_traits<Return(Lambda::*)(Args...) const>
{};

template <typename Lambda, typename Return, typename... Args>
struct lambda_traits<Return(Lambda::*)(Args...) const>
{
    using pointer = typename std::add_pointer<Return(Args...)>::type;

    static pointer to_pointer(Lambda&& lambda)
    {
        static Lambda static_lambda = std::forward<Lambda>(lambda);
        return [](Args... args){
            return static_lambda(std::forward<Args>(args)...);
        };
    }
};

template <typename Lambda>
inline typename lambda_traits<Lambda>::pointer to_pointer(Lambda&& lambda)
{
    return lambda_traits<Lambda>::to_pointer(std::forward<Lambda>(lambda));
}

This can be used as follows to pass a lambda with a capture into a C-style API:


// Function that takes a C-style function pointer as an argument
void call_function(void(*function)())
{
    (*function)();
}

int main()
{
    int x = 42;

    // Pass the lambda to the C-style API
    // This works even though the lambda captures 'x'!
    call_function(to_pointer([x] {
        std::cout << x << std::endl;
        }));
}

Given this, it seems like it should be relatively straightforward to write a similar template that can convert lambdas (including lambdas with captures) generically into std::function objects, but I am struggling to figure out how. (I am not super familiar with template meta-programming techniques so I am a bit lost)

This is what I tried, but it fails to compile:

template <typename Lambda>
struct lambda_traits : lambda_traits<decltype(&Lambda::operator())>
{};

template <typename Lambda, typename Return, typename... Args>
struct lambda_traits<typename std::function<Return(Args...)>> : lambda_traits<typename std::function<Return(Args...)> const>
{};

template <typename Lambda, typename Return, typename... Args>
struct lambda_traits<typename std::function<Return(Args...)> const>
{
    using pointer = typename std::function<Return(Args...)>*;

    static pointer to_pointer(Lambda&& lambda)
    {
        static Lambda static_lambda = std::forward<Lambda>(lambda);
        return [](Args... args) {
            return static_lambda(std::forward<Args>(args)...);
        };
    }
};

template <typename Lambda>
inline typename lambda_traits<Lambda>::pointer to_pointer(Lambda&& lambda)
{
    return lambda_traits<Lambda>::to_pointer(std::forward<Lambda>(lambda));
}

This fails to compile and says that the Lambda template parameter is not being used by the partial specialization.

What is the correct way to do this?

(Note, I am stuck using a C++11 compatible compiler so features from C++14 and beyond are not available)

Draper answered 4/2, 2021 at 0:47 Comment(3)
"I have the following code that can convert a lambda into a C-style function pointer." Very dangerously. If you convert two callables of the same type to a function pointer using that code, the second function pointer will behave exactly like the first function pointer, disregarding any difference in state (captures). This is a bad idea. See also codereview.stackexchange.com/a/255187/49895Sarmentum
Is what you looking for C++17's std::function(lambda) (i.e. the class template argument deduction for std::function)?Sarmentum
Yes that does look like what I need... but I'm stuck using an older compiler so I can only use C++11 features. Is there any way to get that behavior in C++11?Draper
S
4

If you want to convert a callable object to a std::function without specifying the signature of the std::function, this is exactly what C++17's deduction guides for std::function are for. We just need to implement a version of that for C++11. Note that this only works for callables with a non-overloaded operator(); otherwise, there is no way to do this.

#include <functional>
#include <utility> // std::declval

// Using these functions just for the return types, so they don't need an implementation.

// Support function pointers
template <typename R, typename... ArgTypes>
auto deduce_std_function(R(*)(ArgTypes...)) -> std::function<R(ArgTypes...)>;

// Support callables (note the _impl on the name).
// Many overloads of this to support different const qualifiers and
// ref qualifiers. Technically should also support volatile, but that
// doubles the number of overloads and isn't needed for this illustration.
template <typename F, typename R, typename... ArgTypes>
auto deduce_std_function_impl(R(F::*)(ArgTypes...)) -> std::function<R(ArgTypes...)>;

template <typename F, typename R, typename... ArgTypes>
auto deduce_std_function_impl(R(F::*)(ArgTypes...) const) -> std::function<R(ArgTypes...)>;

template <typename F, typename R, typename... ArgTypes>
auto deduce_std_function_impl(R(F::*)(ArgTypes...) &) -> std::function<R(ArgTypes...)>;

template <typename F, typename R, typename... ArgTypes>
auto deduce_std_function_impl(R(F::*)(ArgTypes...) const&) -> std::function<R(ArgTypes...)>;

template <typename F, typename R, typename... ArgTypes>
auto deduce_std_function_impl(R(F::*)(ArgTypes...) &&) -> std::function<R(ArgTypes...)>;

template <typename F, typename R, typename... ArgTypes>
auto deduce_std_function_impl(R(F::*)(ArgTypes...) const&&) -> std::function<R(ArgTypes...)>;

// To deduce the function type for a callable, get its operator() and pass that to
// the _impl functions above.
template <typename Function>
auto deduce_std_function(Function)
    -> decltype(deduce_std_function_impl(&Function::operator()));

template <typename Function>
using deduce_std_function_t = decltype(deduce_std_function(std::declval<Function>()));

template <typename F>
auto to_std_function(F&& fn) -> deduce_std_function_t<F> {
    return deduce_std_function_t<F>(std::forward<F>(fn));
}

Demo


A more detailed explanation

We need to deduce the function type for the std::function<...>. So we need to implement some kind of deduce_std_function that figures out the function type. There are several options for implementing this:

  • Make a function_traits type which figures out the function type for us (similar to your lambda_traits).
  • Implement deduce_std_function as an overload set, where the return type of the overloads is the deduced type.

I chose the latter because it mimics deduction guides. The former would work too, but I thought that this method might be easier (function boilerplate is smaller than struct boilerplate).

The easy case

Looking at the documentation for std::function's deduction guides, there is an easy one:

template<class R, class... ArgTypes>
function(R(*)(ArgTypes...)) -> function<R(ArgTypes...)>;

That can be easily translated:

template <typename R, typename... ArgTypes>
auto deduce_std_function(R(*)(ArgTypes...)) -> std::function<R(ArgTypes...)>;

Basically, given any function pointer R(*)(ArgTypes...), the type we want is std::function<R(ArgTypes...)>.

The trickier case

The documentation states the second case as:

This overload only participates in overload resolution if &F::operator() is well-formed when treated as an unevaluated operand and decltype(&F::operator()) is of the form R(G::*)(A...) (optionally cv-qualified, optionally noexcept, optionally lvalue reference qualified) for some class type G. The deduced type is std::function<R(A...)>.

That's a mouthful. However, the key idea here is the pieces:

  • "decltype(&F::operator()) is of the form R(G::*)(A...)"
  • "The deduced type is std::function<R(A...)>"

This means that we need to get the pointer-to-member-function for operator() and use the signature of that pointer-to-member-function as the signature of the std::function.

That's where this comes from:

template <typename Function>
auto deduce_std_function(Function)
    -> decltype(deduce_std_function_impl(&Function::operator()));

We delegate to deduce_std_function_impl because we need to deduce the signature for the pointer-to-member-function &Function::operator().

The interesting overload of that impl function is:

template <typename F, typename R, typename... ArgTypes>
auto deduce_std_function_impl(R(F::*)(ArgTypes...)) -> std::function<R(ArgTypes...)>;

In short, we're grabbing the signature (the R ... (ArgTypes...) bit) of the pointer-to-member-function and using that for std::function. The remaining part of the syntax (the (F::*) bit) is just the syntax for a pointer-to-member-function. R(F::*)(ArgTypes...) is the type of a pointer-to-member-function for class F with the signature R(ArgTypes...) and no const, volatile, or reference qualifiers.

But wait! We want to support const and reference qualifiers (you may wish to add support for volatile too). So we need to duplicate the deduce_std_function_impl above, once for each qualifier:

Signature Class Declaration
R(F::*)(ArgTypes...) void operator()();
R(F::*)(ArgTypes...) const void operator()() const;
R(F::*)(ArgTypes...) & void operator()() &;
R(F::*)(ArgTypes...) const& void operator()() const&;
R(F::*)(ArgTypes...) && void operator()() &&;
R(F::*)(ArgTypes...) const&& void operator()() const&&;
Sarmentum answered 4/2, 2021 at 1:16 Comment(3)
That does the trick, you are a champion! Thank you so much! Now I just need to spend the next couple days wrapping my head around exactly how this works haha.Draper
Thanks for the in-depth explanation, I have a lot to learn with template meta-programming so this was all very helpful. :)Draper
@tjwrona1992 No problem! I figured if you'd have to spend a couple days to wrap your head around it, an in-depth explanation would probably help. Glad to see that it was helpful.Sarmentum

© 2022 - 2024 — McMap. All rights reserved.