将捕获lambda表达式作为函数指针传递

338

能否将lambda函数作为函数指针传递?如果可以,那么我一定做错了什么,因为我得到了编译错误。

考虑以下示例

using DecisionFn = bool(*)();

class Decide
{
public:
    Decide(DecisionFn dec) : _dec{dec} {}
private:
    DecisionFn _dec;
};

int main()
{
    int x = 5;
    Decide greaterThanThree{ [x](){ return x > 3; } };
    return 0;
}

当我尝试编译这个时,我会得到以下编译错误:

In function 'int main()':
17:31: error: the value of 'x' is not usable in a constant expression
16:9:  note: 'int x' is not const
17:53: error: no matching function for call to 'Decide::Decide(<brace-enclosed initializer list>)'
17:53: note: candidates are:
9:5:   note: Decide::Decide(DecisionFn)
9:5:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'DecisionFn {aka bool (*)()}'
6:7:   note: constexpr Decide::Decide(const Decide&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'const Decide&'
6:7:   note: constexpr Decide::Decide(Decide&&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'Decide&&'

这是一个相当困难的错误信息,但我觉得我理解了它的含义:lambda不能被视为constexpr,所以我不能将其作为函数指针传递?我已经尝试将x设置为constexpr,但似乎没有起到帮助作用。


69
只有当lambda没有捕捉任何变量时,它才可以衰减为函数指针。 - Jarod42
4
http://blogs.msdn.com/b/oldnewthing/archive/2015/02/20/10594680.aspx - BoBTFish
为了后人,上面链接的博客文章现在位于https://devblogs.microsoft.com/oldnewthing/20150220-00/?p=44623。 - warrenm
相关:https://dev59.com/Xmox5IYBdhLWcg3wqmBt#9054802 和 https://dev59.com/qF8e5IYBdhLWcg3wVJPG - Gabriel Staples
10个回答

301
如果一个lambda表达式没有捕获变量,那么它只能被转换为函数指针,根据draft C++11 standard5.1.2[expr.prim.lambda]所述(我强调):

对于一个没有lambda捕获的lambda表达式,其闭包类型具有一个公共的非虚拟的非显式const转换函数为函数指针,与闭包类型的函数调用操作符具有相同的参数和返回类型。此转换函数返回的值应为一个函数的地址,当调用该函数时,具有与调用闭包类型的函数调用操作符相同的效果。

注意,cppreference也在他们的Lambda functions部分涵盖了这一点。

因此,以下替代方案将起作用:

typedef bool(*DecisionFn)(int);

Decide greaterThanThree{ []( int x ){ return x > 3; } };

这个也是:

typedef bool(*DecisionFn)();

Decide greaterThanThree{ [](){ return true ; } };

正如5gon12eder所指出的那样,您还可以使用std::function,但请注意std::function很重量级,因此这不是一种没有代价的权衡。


7
旁注:C语言常用的一种解决方案是将 void* 作为唯一参数传递。通常称之为“用户指针”。它相对较轻,但需要 malloc 出一些空间。请注意,这不是解释。 - anon

147

Shafik Yaghmour的回答正确解释了为什么在Lambda表达式有捕获时无法将其作为函数指针传递。我想展示两种解决方法。

  1. 使用 std::function 而不是原始函数指针。

    这是一个非常干净的解决方案。请注意,它包括一些额外的开销用于类型擦除(可能涉及虚函数调用)。

  2. #include <functional>
    #include <utility>
    
    struct Decide
    {
      using DecisionFn = std::function<bool()>;
      Decide(DecisionFn dec) : dec_ {std::move(dec)} {}
      DecisionFn dec_;
    };
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree { [x](){ return x > 3; } };
    }
    
  3. 使用不捕获任何内容的lambda表达式。

    由于您的谓词实际上只是一个布尔常量,因此以下内容可以快速解决当前的问题。有关为什么以及如何工作的良好解释,请参见此答案

  4. // Your 'Decide' class as in your post.
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree {
        (x > 3) ? [](){ return true; } : [](){ return false; }
      };
    }
    

4
请参考这个问题以了解为什么它可行。 - Shafik Yaghmour
请注意,通常情况下,如果您在编译时知道捕获数据,您可以将其转换为类型数据,然后您又回到了没有捕获的lambda函数 - 请参见我刚刚针对另一个问题撰写的此答案(感谢@5gon12eder在这里的答案)。 - dan-man
对象的生命周期不应该比指针函数长吗?我想在glutReshapeFunc中使用它。 - ar2015
我不推荐这个建议,因为那些看似神奇的东西往往会引入新的错误和相关的实践。如果你想使用std::function,你应该了解所有可能的用法,因为有些用法可能并不适合你。 - TheNegative
4
这并没有回答问题。如果能够使用std::function或lambda,为什么不使用呢?至少它是更易读的语法。通常需要使用函数指针与C库 (实际上,与任何外部库) 进行交互,当然,您不能修改它以接受一个std::function或lambda。 - Hi-Angel

65

即使是被捕获的Lambda表达式,也可以被处理为函数指针(成员函数指针)。这很棘手,因为Lambda表达式不是简单的函数,实际上是具有operator()的对象。

当你很有创意时,可以利用这一点!想象一下像std::function风格的“函数”类。如果保存该对象,则还可以使用函数指针。

要使用函数指针,可以使用以下内容:

int first = 5;
auto lambda = [=](int x, int z) {
    return x + z + first;
};
int(decltype(lambda)::*ptr)(int, int)const = &decltype(lambda)::operator();
std::cout << "test = " << (lambda.*ptr)(2, 3) << std::endl;

要建立一个类似于“std::function”能够工作的类,首先需要一个可以存储对象和函数指针的类/结构体。此外,您需要一个operator()以执行它:

// OT => Object Type
// RT => Return Type
// A ... => Arguments
template<typename OT, typename RT, typename ... A>
struct lambda_expression {
    OT _object;
    RT(OT::*_function)(A...)const;

    lambda_expression(const OT & object)
        : _object(object), _function(&decltype(_object)::operator()) {}

    RT operator() (A ... args) const {
        return (_object.*_function)(args...);
    }
};

现在,您可以运行捕获的和非捕获的 lambda 表达式,就像使用原始表达式一样:

auto capture_lambda() {
    int first = 5;
    auto lambda = [=](int x, int z) {
        return x + z + first;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

auto noncapture_lambda() {
    auto lambda = [](int x, int z) {
        return x + z;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

void refcapture_lambda() {
    int test;
    auto lambda = [&](int x, int z) {
        test = x + z;
    };
    lambda_expression<decltype(lambda), void, int, int>f(lambda);
    f(2, 3);

    std::cout << "test value = " << test << std::endl;
}

int main(int argc, char **argv) {
    auto f_capture = capture_lambda();
    auto f_noncapture = noncapture_lambda();

    std::cout << "main test = " << f_capture(2, 3) << std::endl;
    std::cout << "main test = " << f_noncapture(2, 3) << std::endl;

    refcapture_lambda();

    system("PAUSE");
    return 0;
}

这段代码可以在VS2015上运行。

更新于04.07.17:

template <typename CT, typename ... A> struct function
: public function<decltype(&CT::operator())(A...)> {};

template <typename C> struct function<C> {
private:
    C mObject;

public:
    function(const C & obj)
        : mObject(obj) {}

    template<typename... Args> typename 
    std::result_of<C(Args...)>::type operator()(Args... a) {
        return this->mObject.operator()(a...);
    }

    template<typename... Args> typename 
    std::result_of<const C(Args...)>::type operator()(Args... a) const {
        return this->mObject.operator()(a...);
    }
};

namespace make {
    template<typename C> auto function(const C & obj) {
        return ::function<C>(obj);
    }
}

int main(int argc, char ** argv) {
   auto func = make::function([](int y, int x) { return x*y; });
   std::cout << func(2, 4) << std::endl;
   system("PAUSE");
   return 0;
}

3
我已经添加了自己代码的简短版本。使用简单的auto f = make::function(lambda)应该可以工作。 但我相当确定你会发现许多情况下我的代码无法正常工作。 std::function比这个更加完善,应该是你在工作时的首选。这里只是用于教育和个人使用。 - Noxxer
19
这个解决方案涉及通过实现operator()来调用lambda表达式,所以如果我读对了的话,我认为不可能使用C风格的函数指针来调用lambda表达式,对吗?这就是原问题所要求的。 - Remy Lebeau
22
你声称lambda表达式可以被视作函数指针进行处理,但你并没有这样做。相反,你创建了另一个对象来容纳该lambda表达式,但它实际上没有做任何事情,你本可以直接使用原始的lambda表达式。 - Passer By
22
这不是“将捕获的lambda表达式作为函数指针传递”。这是将捕获的lambda表达式作为一个包含函数指针等其他内容的对象进行传递。两者之间有很大的区别。 - n. m.
5
иҝҷдёҚжҳҜдёҖдёӘзӯ”жЎҲпјҢеӣ дёәдҪ ж— жі•е°Ҷlambda.*ptrдј йҖ’з»ҷйңҖиҰҒеҮҪж•°жҢҮй’Ҳзҡ„APIгҖӮдҪ дјҡ收еҲ°error: invalid use of non-static member function of typeй”ҷиҜҜжҸҗзӨәпјҢеӣ дёәиҝҷжҳҜйқһйқҷжҖҒжҲҗе‘ҳеҮҪж•°зҡ„дҪҝз”Ёж–№ејҸдёҚжӯЈзЎ®гҖӮ - Vladimir Nikishkin
显示剩余11条评论

30
捕获lambda表达式不能转换为函数指针,正如this answer所指出的。
然而,将函数指针提供给只接受函数指针的API通常是一件非常麻烦的事情。最常被引用的方法是提供一个函数并调用一个静态对象。
static Callable callable;
static bool wrapper()
{
    return callable();
}

这太繁琐了。我们将这个想法进一步发展,自动化创建“包装器”的过程,让生活变得更加轻松。
#include<type_traits>
#include<utility>
#include<new>

template<int, typename Callable, typename Ret, typename... Args>
auto fnptr_(Callable&& c, Ret (*)(Args...))
{
    static std::decay_t<Callable> storage = std::forward<Callable>(c);
    static bool used = false;
    if(used)
    {
        using type = decltype(storage);
        storage.~type();
        new (&storage) type(std::forward<Callable>(c));
    }
    used = true;

    return [](Args... args) -> Ret {
        auto& c = *std::launder(&storage);
        return Ret(c(std::forward<Args>(args)...));
    };
}

template<typename Fn, int N = 0, typename Callable>
Fn* fnptr(Callable&& c)
{
    return fnptr_<N>(std::forward<Callable>(c), (Fn*)nullptr);
}

并将其用作

void foo(void (*fn)())
{
    fn();   
}

int main()
{
    int i = 42;
    auto fn = fnptr<void()>([i]{std::cout << i;});
    foo(fn);  // compiles!
}

直播

这实际上是在每次出现fnptr时声明一个匿名函数。

请注意,对fnptr的调用会覆盖先前写入的相同类型的可调用项。我们通过int参数N在一定程度上解决了这个问题。

std::function<void()> func1, func2;
auto fn1 = fnptr<void(), 1>(func1);
auto fn2 = fnptr<void(), 2>(func2);  // different function

1
强制声明N整数将是一种优雅的方式,以便在编译时记住客户端,避免覆盖函数指针。 - fiorentinoing
@DrownedSuccess 如果你有问题,请发布一个新的问题。请也阅读[ask]。 - undefined
@路人问题发布在这里:https://stackoverflow.com/q/77207319/17920058 - undefined

3

一个类似的答案,但我将其修改为您无需指定返回指针类型(请注意,通用版本需要C++20):

#include <iostream>


template<typename Function>
struct function_traits;

template <typename Ret, typename... Args>
struct function_traits<Ret(Args...)> {
    typedef Ret(*ptr)(Args...);
};

template <typename Ret, typename... Args>
struct function_traits<Ret(*const)(Args...)> : function_traits<Ret(Args...)> {};

template <typename Cls, typename Ret, typename... Args>
struct function_traits<Ret(Cls::*)(Args...) const> : function_traits<Ret(Args...)> {};

using voidfun = void(*)();

template <typename F>
voidfun lambda_to_void_function(F lambda) {
    static auto lambda_copy = lambda;

    return []() {
        lambda_copy();
    };
}

// requires C++20
template <typename F>
auto lambda_to_pointer(F lambda) -> typename function_traits<decltype(&F::operator())>::ptr {
    static auto lambda_copy = lambda;
    
    return []<typename... Args>(Args... args) {
        return lambda_copy(args...);
    };
}



int main() {
    int num;

    void(*foo)() = lambda_to_void_function([&num]() {
        num = 1234;
    });
    foo();
    std::cout << num << std::endl; // 1234

    int(*bar)(int) = lambda_to_pointer([&](int a) -> int {
        num = a;
        return a;
    });
    std::cout << bar(4321) << std::endl; // 4321
    std::cout << num << std::endl; // 4321
}

1
static auto lambda_copy = lambda; 静态意味着 lambda 只有一个副本。 - Moritz
有没有办法使这个线程安全? - vlovero
你说这意味着 lambda 只有一个副本,但在我的代码(单线程)中,我能够多次调用 lambda_to_pointer,保存结果,并且没有混淆地调用结果。我错过了什么? - benathon

2

虽然不是直接的答案,但可以使用“函数对象”模板模式稍作变化,以隐藏lambda类型的具体细节,并使代码变得简洁易懂。

我不确定您想如何使用decide类,因此我必须通过向该类添加一个使用它的函数来扩展该类。完整示例请参见:https://godbolt.org/z/jtByqE

您的类的基本形式可能如下所示:

template <typename Functor>
class Decide
{
public:
    Decide(Functor dec) : _dec{dec} {}
private:
    Functor _dec;
};

当你将函数类型作为类类型的一部分传递时,用法如下:

auto decide_fc = [](int x){ return x > 3; };
Decide<decltype(decide_fc)> greaterThanThree{decide_fc};

我不确定为什么要捕获x,在我看来更合理的做法是将其作为参数传递给lambda表达式,这样你就可以像这样使用:

int result = _dec(5); // or whatever value

请查看链接获取完整的示例


1

这是解决方案的另一种变体。C++14(可以转换为C++11),支持返回值、不可拷贝和可变Lambda。如果不需要可变Lambda,可以通过删除匹配非const版本的特化并嵌入impl_impl来使其更短。

对于那些好奇的人,它之所以有效,是因为每个Lambda都是独特的(是不同的类),因此调用to_f会生成一个为该Lambda生成唯一的静态C风格函数,并可以访问它。

template <class L, class R, class... Args> static auto impl_impl(L l) {
  static_assert(!std::is_same<L, std::function<R(Args...)>>::value,
                "Only lambdas are supported, it is unsafe to use "
                "std::function or other non-lambda callables");

    static L lambda_s = std::move(l);
    return +[](Args... args) -> R { return lambda_s(args...); };
}

template <class L>
struct to_f_impl : public to_f_impl<decltype(&L::operator())> {};
template <class ClassType, class R, class... Args>
struct to_f_impl<R (ClassType::*)(Args...) const> {
  template <class L> static auto impl(L l) {
    return impl_impl<L, R, Args...>(std::move(l));
  }
};
template <class ClassType, class R, class... Args>
struct to_f_impl<R (ClassType::*)(Args...)> {
  template <class L> static auto impl(L l) {
    return impl_impl<L, R, Args...>(std::move(l));
  }
};

template <class L> auto to_f(L l) { return to_f_impl<L>::impl(std::move(l)); }

注意,这也适用于其他可调用对象,如std::function,但最好不要使用它,因为与lambda不同,类似std::function的对象不会生成唯一类型,因此内部模板及其内部静态将被所有具有相同签名的函数重用/共享,这很可能不是我们想要的。我已经明确禁止了std::function,但还存在更多我不知道如何以通用方式禁止的情况。

你可以使用带有模板的ODR来拒绝使用相同签名的std::function进行多次调用,而不是使用static_assert。通常,您将使用给定参数对template<T> detail::someObj进行特化。如果使用相同签名两次调用它,则编译器将拒绝代码(这正是您想要的)。由于lambda即使具有相同的签名也是唯一的对象,因此它们也可以正常工作。 - xryl669

0

使用lambda表达式作为C函数指针的快捷方式如下:

最初的回答:

"auto fun = +[](){}"

使用Curl作为例子(curl调试信息),最初的回答。
auto callback = +[](CURL* handle, curl_infotype type, char* data, size_t size, void*){ //add code here :-) };
curl_easy_setopt(curlHande, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curlHande,CURLOPT_DEBUGFUNCTION,callback);

11
该 lambda 表达式没有捕获任何变量。原帖的问题在于变量的捕获,而不在于推断函数指针类型(这是 + 技巧的作用)。 - Sneftel

-2

虽然模板方法在各种情况下都很聪明,但重要的是要记住 lambda 的生命周期和捕获变量。如果将使用任何形式的 lambda 指针,并且 lambda 不是向下延续,则只应使用复制 [=] lambda。即使如此,如果捕获指向堆栈上变量的指针的生命周期(堆栈展开)比 lambda 的生命周期短,则捕获仍然是不安全的。

捕获 lambda 作为指针的更简单的解决方案是:

auto pLamdba = new std::function<...fn-sig...>([=](...fn-sig...){...});

例如,new std::function<void()>([=]() -> void {...}

记得稍后delete pLamdba以确保不会泄漏lambda内存。这里需要注意的秘密是lambda可以捕获lambda(问问自己这是如何实现的),并且为了使std::function通用工作,lambda实现需要包含足够的内部信息来提供对lambda(和捕获)数据大小的访问权限(这就是为什么delete应该工作[运行捕获类型的析构函数])。


4
为什么要费心使用 new -- std::function 已经将 lambda 存储在堆上,并且避免了需要记得调用 delete 的麻烦。 - Chris Dodd
3
“捕获lambda作为指针”,你的意思是在std::function内部进行类型抹除...但是OP明显想要一个自由函数指针。拥有对std::function的指针,通过建议无意义的原始new反模式,并不符合Q的相关要求。” - underscore_d

-4

正如其他人所提到的,您可以使用Lambda函数替代函数指针。我在我的C++接口中使用这种方法来解决F77 ODE求解器RKSUITE的问题。

//C interface to Fortran subroutine UT
extern "C"  void UT(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

// C++ wrapper which calls extern "C" void UT routine
static  void   rk_ut(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

//  Call of rk_ut with lambda passed instead of function pointer to derivative
//  routine
mathlib::RungeKuttaSolver::rk_ut([](double* T,double* Y,double* YP)->void{YP[0]=Y[1]; YP[1]= -Y[0];}, TWANT,T,Y,YP,YMAX,WORK,UFLAG);

1
这是个玩笑吗?double*,double* double* double* double* ... 看不懂。 - Denis G. Labrecque

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