Detect if C++ lambda can be converted to function pointer
Asked Answered
T

3

9

I have some code that generates assembly for a JIT idea I'm working on. I use meta-programming to generate calls by analyzing the function type and then generating the correct assembly to call it. I recently wanted to add lambda support, and lambdas have two versions, non-capturing (normal __cdecl function call) and capturing (__thiscall, member-function call with the lambda object as context).

__thiscall is slightly more expensive so I'd like to avoid it whenever possible, and I'd also like to avoid having to use different call generation functions depending on the lambda type.

I tried many ways to detect the lambda type via templates and SFINAE and all failed.

Non-capturing lambdas have an ::operator function_type* which one can use to convert them to function pointers, while capturing lambdas don't.

Relevant C++ spec: http://en.cppreference.com/w/cpp/language/lambda

Any ideas?

edit I'd like to have a solution that works for vs 2013/2015, gcc and clang

Test code follows

#include <utility>

    //this doesn't work
    template < class C, class T >
    struct HasConversion {
        static int test(decltype(std::declval<C>().operator T*, bool()) bar) {
            return 1;
        }

        static int test(...) {
            return 0;
        }
    };

    template <class C>
    void lambda_pointer(C lambda) {
        int(*function)() = lambda;

        printf("Lambda function: %p without context\n", function);
    }

    template <class C>
    void lambda_pointer_ctx(C lambda) {
        int(C::*function)() const = &C::operator();

        void* context = &lambda;

        printf("Lambda function: %p with context: %p\n", function, context);
    }

    int main() {
        int a;

        auto l1 = [] {
            return 5;
        };

        auto l2 = [a] {
            return a;
        };


        //non capturing case

        //works as expected
        lambda_pointer(l1);

        //works as expected (ctx is meaningless and not used)
        lambda_pointer_ctx(l1);



        //lambda with capture (needs context)

        //fails as expected
        lambda_pointer(l1);

        //works as expected (ctx is a pointer to class containing the captures)
        lambda_pointer_ctx(l1);

        /*
        //this doesn't work :<
        typedef int afunct() const;

        HasConversion<decltype(l1), afunct>::test(0);
        HasConversion<decltype(l2), afunct>::test(0);
        */


        return 0;
    }
Tripura answered 15/8, 2015 at 13:45 Comment(6)
Do you know the signature of the lambda? That makes it a bit cleaner if you do.Benny
If you knew the signature, you could leverage std::is_assignable<void(*&)(int), decltype(lambda)>{}, or, in your case std::is_assignable<afunct*&, decltype(lambda)>{} with typedef void afunct(int);Tolerable
No, this has to work for any signature -- I infer the call to be generated from the signatureTripura
That is interesting though, I do infer the signature for the lambda and it works on all casesTripura
After testing all of the solutions so far, this is the only one that works for vs2013 && 2015! Trying to see if I can bend it to be generic :DTripura
Okay, after refining this idea a bit more it works, with function type inference! Gist tested on gcc 4.8+, clang 3.3+ and vs 2013. @PiotrSkotnicki can you expand it to an answer so that I accept?Tripura
T
7

If you know the signature of a function you want your lambda to be converted to, you can leverage the std::is_assignable trait:

auto lambda = [] (char, double) -> int { return 0; };
using signature = int(char, double);
static_assert(std::is_assignable<signature*&, decltype(lambda)>::value, "!");

DEMO

This way it can work also with generic lambdas.


I'd like to have a solution that works for vs 2013/2015, gcc and clang

If you don't know the signature, here's an approach that is an alternative to checking whether + triggers an implicit conversion. This one exploits the std::is_assignable test and verifies whether a lambda is assignable to a function pointer with the same signature as the lambda's function call operator. (Just like a test with unary operator plus, this doesn't work with generic lambdas. But in C++11 there are no generic lambdas).

#include <type_traits>

template <typename T>
struct identity { using type = T; };

template <typename...>
using void_t = void;

template <typename F>
struct call_operator;

template <typename C, typename R, typename... A>
struct call_operator<R(C::*)(A...)> : identity<R(A...)> {};

template <typename C, typename R, typename... A>
struct call_operator<R(C::*)(A...) const> : identity<R(A...)> {};

template <typename F>
using call_operator_t = typename call_operator<F>::type;

template <typename, typename = void_t<>>
struct is_convertible_to_function
    : std::false_type {};

template <typename L>
struct is_convertible_to_function<L, void_t<decltype(&L::operator())>>
    : std::is_assignable<call_operator_t<decltype(&L::operator())>*&, L> {};

Test:

int main()
{
    auto x = [] { return 5; };
    auto y = [x] { return x(); };

    static_assert(is_convertible_to_function<decltype(x)>::value, "!");
    static_assert(!is_convertible_to_function<decltype(y)>::value, "!");
}

GCC, VC++, Clang++

Tolerable answered 17/8, 2015 at 8:8 Comment(1)
I ended up using a similar (but less well written) solution. Accepting because of better portability.Tripura
R
3

Non-capturing lambdas have a very interesting property : they can convert to an adequate function pointer, but they can also do so implicitly when you apply unary operator + to them. Thus :

template<class...> using void_t = void;

template <class T, class = void>
struct has_capture : std::true_type {};

template <class T>
struct has_capture<T, void_t<decltype(+std::declval<T>())>> : std::false_type {};

int main() {
    auto f1 = []{};
    auto f2 = [&f1]{};

    static_assert(!has_capture<decltype(f1)>{}, "");
    static_assert( has_capture<decltype(f2)>{}, "");
}
Ribonuclease answered 15/8, 2015 at 13:58 Comment(5)
Annoyingly, one version of MSVC broke this: it supplied multiple calling conventions, and unary + became ambiguous. 2015 pre release maybe? Dunno if it shipped.Benny
@Yakk no idea, but if it's true, well that sucks :/Ribonuclease
It is indeed broken on VS 2013, installing 2015 to testTripura
2015 still has the ambiguity issue. On both compilers has_capture always returns false. Works as expected on GCC 5.1, though. Any idea where operator+ is documented? I didn't see it while reading the spec...Tripura
@StefanosK.M.P. I think this answer covers it all :)Ribonuclease
P
3

The HasConversion approach you have going is a holdover from C++03. The idea there was to use the different return types of the overloads of test (have one return a char and the other a long, for instance) and check that the sizeof() of the return type matches what you expect.

Once we're on C++11 though, there are much better methods. We can, for instance, use void_t:

template <typename... >
using void_t = void;

to write a type trait:

template <typename T, typename = void>
struct has_operator_plus : std::false_type { };

template <typename T>
struct has_operator_plus<T, 
    void_t<decltype(+std::declval<T>())>>
: std::true_type { };

int main() {
    auto x = []{ return 5; };
    auto y = [x]{ return x(); };

    std::cout << has_operator_plus<decltype(x)>::value << std::endl; // 1
    std::cout << has_operator_plus<decltype(y)>::value << std::endl; // 0
}
Psychogenic answered 15/8, 2015 at 13:59 Comment(3)
Can't wait until C++17: template <typename T> concept bool has_operator_plus = requires (T t) {+t;};Comose
Sadly, it seems like there's some issue on vs 2013 (and 2015) that makes it return always 1. It does work correctly when tested on gcc 5.2, though. Thx for the void_t trick! I'm not really up to par with C++11 patterns yet, even my C++03 is pretty rusty.Tripura
This is great but also somewhat misleading in the context given. As far as I know (but please correct me if I'm wrong), lambda's don't have a unary addition operator operator+ defined. Non-capturing lambda's are simply equipped with an implicit conversion operator to function-pointer type, which is invoked when requesting an operation that requires a scalar type. Therefore, this trait could have a more suitable name in my opinion.Kapp

© 2022 - 2024 — McMap. All rights reserved.