Определить, можно ли преобразовать лямбду C++ в указатель на функцию

у меня есть код, который генерирует сборку для идеи JIT, над которой я работаю. Я использую мета-программирование для генерации вызовов, анализируя тип функции, а затем генерируя правильную сборку для ее вызова. Недавно я хотел добавить поддержку лямбда, а у лямбда есть две версии, без захвата (обычный вызов функции __cdecl) и захвата (__thiscall, вызов функции-члена с лямбда-объектом в качестве контекста).

_ _ thiscall немного дороже, поэтому я хотел бы избежать этого всякий раз, когда возможно, и я также хотел бы избежать использования различных функций генерации вызовов в зависимости от типа лямбда.

Я пробовал много способов обнаружить лямбда-тип с помощью шаблонов и SFINAE, и все не удалось.

Не захватывающие лямбды имеют ::operator function_type* который можно использовать для преобразования их в указатели функций, а захват лямбд-нет.

соответствующая спецификация C++:http://en.cppreference.com/w/cpp/language/lambda

любой идеи?

редактировать Я хотел бы иметь решение, которое работает для vs 2013/2015, gcc и clang

тестовый код следующим образом

#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 contextn", 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: %pn", 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;
    }

3 ответов


если вы знаете сигнатуру функции, в которую вы хотите преобразовать лямбду, вы можете использовать std::is_assignable особенность:

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

демо

таким образом, он может работать также с универсальными лямбдами.


я хотел бы иметь решение, которое работает для vs 2013/2015, gcc и clang

если вы не знаете подпись, вот подход, который является альтернатива проверке + вызывает неявное преобразование. Этот эксплуатирует std::is_assignable проверить и проверить, назначается ли лямбда указателю функции с той же сигнатурой, что и оператор вызова функции лямбда. (Как и тест с унарным оператором plus, это не работает с общими лямбдами. Но в C++11 нет общих лямбд).

#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> {};

The HasConversion подход, который у вас есть, - это пережиток C++03. Идея состояла в том, чтобы использовать различные возвращаемые типы перегрузок test (есть один возврат a char и long, например) и проверьте, что sizeof() типа возврата соответствует тому, что вы ожидаете.

как только мы на C++11, есть гораздо лучшие методы. Мы можем, например, использовать void_t:

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

чтобы написать признак типа:

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
}

Не захватывающие лямбды имеют очень интересное свойство: они могут преобразовываться в адекватный указатель функции, но они также могут делать это неявно, когда вы применяете унарный operator + на них. Таким образом :

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)>{}, "");
}