检测C++ lambda是否可以转换为函数指针

9
我有一些生成JIT想法的汇编代码。我使用元编程分析函数类型并生成正确的汇编代码来调用它。我最近想添加lambda支持,而lambda有两个版本,非捕获(普通__cdecl函数调用)和捕获(__thiscall,以lambda对象为上下文的成员函数调用)。
__thiscall稍微更昂贵,因此我希望尽可能避免使用它,并且我也希望避免根据lambda类型使用不同的调用生成函数。
我尝试了许多通过模板和SFINAE检测lambda类型的方法,但都失败了。
非捕获lambda有一个::operator function_type*,可以用它将它们转换为函数指针,而捕获lambda则没有。
相关的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 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;
    }

你知道lambda的签名吗?如果你知道,代码会更加简洁。 - Yakk - Adam Nevraumont
如果您知道签名,您可以利用 std::is_assignable<void(*&)(int), decltype(lambda)>{} 或者在您的情况下使用 typedef void afunct(int);std::is_assignable<afunct*&, decltype(lambda)>{} - Piotr Skotnicki
不,这必须适用于任何签名--我推断调用将从签名生成 - Stefanos K. M. P.
虽然很有趣,但我确实推断了lambda的签名,并且它适用于所有情况。 - Stefanos K. M. P.
在测试了目前所有的解决方案之后,这是唯一一个适用于 vs2013 和 2015 的解决方案!试图看看是否可以将其弯曲成通用的 :D - Stefanos K. M. P.
好的,经过进一步完善这个想法,它可以工作了,使用函数类型推断!Gist 在 gcc 4.8+、clang 3.3+ 和 vs 2013 上进行了测试。@PiotrSkotnicki,你能否将其扩展为一个答案,以便我接受? - Stefanos K. M. P.
3个回答

7
如果你知道想要将lambda转换为的函数的签名,你可以利用std::is_assignable特性:
auto lambda = [] (char, double) -> int { return 0; };
using signature = int(char, double);
static_assert(std::is_assignable<signature*&, decltype(lambda)>::value, "!");

演示

这样也可以与通用的lambda一起使用。


我想要一个适用于 vs 2013/2015、gcc 和 clang 的解决方案。如果你不知道签名,这里有一种方法是替代检查 + 是否会触发隐式转换的方式。它利用了 std::is_assignable 测试并验证 lambda 表达式是否可分配给具有与 lambda 函数调用运算符相同签名的函数指针。(就像使用一元操作符加号的测试一样,这不能用于通用 lambda。但在 C++11 中没有通用 lambda)。
#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> {};

测试:

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++

翻译后结果为:

{{链接1:GCC},{链接2:VC ++},{链接3:Clang ++}}


1
我最终使用了一个类似的(但写得不太好)解决方案。接受它是因为它更具可移植性。 - Stefanos K. M. P.

3
非捕获lambda有一个非常有趣的特性:它们可以转换为适当的函数指针,但是当你对它们应用一元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)>{}, "");
}

令人烦恼的是,MSVC的一个版本出现了问题:它提供了多个调用约定,并且一元运算符+变得模棱两可。2015年的预发布版可能有这个问题?不确定是否已经发货。 - Yakk - Adam Nevraumont
@Yakk 我不知道,但如果这是真的,那太糟糕了 :/ - Quentin
确实在VS 2013上出现了故障,安装2015版本进行测试。 - Stefanos K. M. P.
2015年仍存在歧义问题。在两个编译器上,has_capture始终返回false。但在GCC 5.1上按预期工作。有没有关于operator+的文档?我在阅读规范时没有看到它... - Stefanos K. M. P.
1
@StefanosK.M.P. 我认为这个答案已经涵盖了所有 :) - Quentin

3
您正在使用的HasConversion方法是C++03的遗留物。其思想是利用test重载的不同返回类型(例如一个返回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
}

3
迫不及待,直到 C++17: template <typename T> concept bool has_operator_plus = requires (T t) {+t;}; (注:该语句定义了一个名为“has_operator_plus”的模板概念,用于检查类型T是否定义了一元加运算符“+”) - Columbo
很遗憾,似乎在vs 2013(和2015)上存在一些问题,导致它始终返回1。不过,在gcc 5.2上进行测试时,它确实能正常工作。感谢void_t的技巧!我对C++11的模式还不太熟悉,甚至我的C++03也有点生疏了。 - Stefanos K. M. P.
这很棒,但在给定的上下文中有些误导人。据我所知(如果我错了,请纠正我),lambda没有定义一元加法运算符operator+。非捕获lambda只是配备了一个隐式转换运算符到函数指针类型,当请求需要标量类型的操作时会调用该运算符。因此,在我看来,这个特性可能有一个更合适的名称。 - 303

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接