C++11 中的递归 lambda 函数
Posted
技术标签:
【中文标题】C++11 中的递归 lambda 函数【英文标题】:Recursive lambda functions in C++11 【发布时间】:2011-01-05 07:05:47 【问题描述】:我是 C++11 的新手。我正在编写以下递归 lambda 函数,但它无法编译。
sum.cpp
#include <iostream>
#include <functional>
auto term = [](int a)->int
return a*a;
;
auto next = [](int a)->int
return ++a;
;
auto sum = [term,next,&sum](int a, int b)mutable ->int
if(a>b)
return 0;
else
return term(a) + sum(next(a),b);
;
int main()
std::cout<<sum(1,10)<<std::endl;
return 0;
编译错误:
vimal@linux-718q:~/Study/09C++/c++0x/lambda> g++ -std=c++0x sum.cpp
sum.cpp:在 lambda 函数中:
sum.cpp:18:36: 错误:'((<lambda(int, int)>*)this)-><lambda(int, int)>::sum
' 不能用作函数
gcc 版本
gcc 版本 4.5.0 20091231(实验性)(GCC)
但是,如果我将sum()
的声明更改如下,它会起作用:
std::function<int(int,int)> sum = [term,next,&sum](int a, int b)->int
if(a>b)
return 0;
else
return term(a) + sum(next(a),b);
;
有人可以解释一下吗?
【问题讨论】:
这可能是静态声明还是隐式动态声明?mutable
关键字在那里做什么?
不允许捕获具有非自动存储持续时间的变量。你应该这样做:chat.***.com/transcript/message/39298544#39298544
仅供参考,在您的第二个代码 sn-p 中,您的 lambda 过于冗长,请考虑以下更改:std::function<int(int,int)> sum = [&](int a, int b)
如果有人能够回答尾递归优化是否适用于任何解决方案,那将是受欢迎的。
【参考方案1】:
想想 auto 版本和完全指定类型版本之间的区别。 auto 关键字从它初始化的任何内容中推断出它的类型,但是你初始化它需要知道它的类型是什么(在这种情况下,lambda 闭包需要知道它正在捕获的类型) .有点像先有鸡还是先有蛋的问题。
另一方面,完全指定的函数对象的类型不需要“知道”任何关于分配给它的内容,因此 lambda 的闭包同样可以完全了解其捕获的类型。
考虑一下对代码的这种轻微修改,它可能更有意义:
std::function<int(int,int)> sum;
sum = [term,next,&sum](int a, int b)->int
if(a>b)
return 0;
else
return term(a) + sum(next(a),b);
;
显然,这不适用于 auto。递归 lambda 函数运行良好(至少它们在 MSVC 中运行良好,我有使用它们的经验),只是它们与类型推断并不真正兼容。
【讨论】:
我不同意这一点。一旦输入函数体,lambda 的类型就众所周知 - 没有理由不应该在那时推导它。 @DeadMG 但规范禁止在其初始化程序中引用auto
变量。处理初始化程序时,auto 变量的类型尚不清楚。
想知道为什么它没有被标记为“答案”,而 Python 被归类为“答案”?!
@Puppy:但是,在隐式捕获的情况下,为了提高效率,实际上只捕获引用的变量,因此必须解析主体。
除了std::function<int(int, int)>
之外,sum
是否有有效的解释,或者 C++ 规范只是懒得推断?【参考方案2】:
诀窍是将 lambda 实现作为参数提供给自身作为参数,而不是通过捕获。
const auto sum = [term,next](int a, int b)
auto sum_impl=[term,next](int a,int b,auto& sum_ref) mutable
if(a>b)
return 0;
return term(a) + sum_ref(next(a),b,sum_ref);
;
return sum_impl(a,b,sum_impl);
;
计算机科学中的所有问题都可以通过另一个间接层次来解决。我首先在http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/ 发现了这个简单的技巧
它确实需要 C++14,而问题是关于 C++11,但可能对大多数人来说很有趣。
通过std::function
也是可能的,但可能会导致代码变慢。但不总是。看看std::function vs template的答案
这不仅仅是 C++ 的一个特点, 它直接映射到 lambda 演算的数学。来自Wikipedia:
Lambda calculus cannot express this as directly as some other notations:
all functions are anonymous in lambda calculus, so we can't refer to a
value which is yet to be defined, inside the lambda term defining that
same value. However, recursion can still be achieved by arranging for a
lambda expression to receive itself as its argument value
【讨论】:
这似乎比明确使用function<>
更糟糕。我不明白为什么有人会喜欢它。编辑:显然更快。
这比 std::function 好得多,原因有 3 个:它不需要类型擦除或内存分配,它可以是 constexpr 并且它可以与自动(模板化)参数/返回类型一起正常工作
大概这个解决方案还具有可复制的优点,而 std::function 引用不会超出范围?
嗯,尝试时,GCC 8.1 (linux) 抱怨:error: use of ‘[...]’ before deduction of ‘auto’
- 需要明确指定返回类型(另一方面,不需要可变)。
@JohanLundberg 它仅在函数中有另一个返回时才有效(因此可以推断返回类型)-在示例中已经有一个 return 0
因此编译器可以推断出返回类型是int
-- 一般情况下需要指定返回类型。【参考方案3】:
使用 C++14,现在可以很容易地制作一个高效的递归 lambda,而无需产生 std::function
的额外开销,只需几行代码:
template <class F>
struct y_combinator
F f; // the lambda will be stored here
// a forwarding operator():
template <class... Args>
decltype(auto) operator()(Args&&... args) const
// we pass ourselves to f, then the arguments.
return f(*this, std::forward<Args>(args)...);
;
// helper function that deduces the type of the lambda:
template <class F>
y_combinator<std::decay_t<F>> make_y_combinator(F&& f)
return std::forward<F>(f);
你原来的sum
尝试变成了:
auto sum = make_y_combinator([term,next](auto sum, int a, int b) -> int
if (a>b)
return 0;
else
return term(a) + sum(next(a),b);
);
在C++17中,通过CTAD,我们可以添加一个推导指南:
template <class F> y_combinator(F) -> y_combinator<F>;
这消除了对辅助函数的需要。我们可以直接写y_combinator[](auto self, ...)...
。
在 C++20 中,使用 CTAD 进行聚合,不需要推导指南。
在 C++23 中,通过推断这一点,您根本不需要 Y 组合器:
auto sum = [term,next](this auto const& sum, int a, int b) -> int
if (a>b)
return 0;
else
return term(a) + sum(next(a),b);
【讨论】:
Y-combinator 肯定是要走的路。但是你真的应该添加一个非const
重载,以防提供的函数对象有一个非const
调用操作符。并为两者使用 SFINAE 和计算 noexcept
。此外,C++17 中不再需要 maker-function。
@minex 是的,auto sum
复制...但它复制了 reference_wrapper
,这与获取参考相同。在实现中执行一次意味着不会意外复制任何用途。
不知道为什么,但是貌似我的lambda要加上->void
返回类型信息,不然编译失败:godbolt.org/z/WWj14P
@qbolec 编译器需要知道它返回什么,并且没有 return
来提示它,所以有时你只需要提供它(即使在这种情况下它应该是“显然”@ 987654337@)
@Barry,你所说的可能是故事的一部分,但肯定还有更多内容,因为在函数中添加return 42;
似乎还不够——它仍然需要-> int
:@ 987654322@【参考方案4】:
我有另一种解决方案,但仅适用于无状态 lambda:
void f()
static int (*self)(int) = [](int i)->int return i>0 ? self(i-1)*i : 1; ;
std::cout<<self(10);
这里的诀窍是 lambda 可以访问静态变量,您可以将无状态变量转换为函数指针。
您可以将其与标准 lambda 一起使用:
void g()
int sum;
auto rec = [&sum](int i) -> int
static int (*inner)(int&, int) = [](int& _sum, int i)->int
_sum += i;
return i>0 ? inner(_sum, i-1)*i : 1;
;
return inner(sum, i);
;
它在 GCC 4.7 中的工作
【讨论】:
这应该比 std::function 有更好的性能,所以 +1 可以替代。但实际上,在这一点上,我想知道使用 lambdas 是否是最好的选择;) 如果你有一个无状态的 lambda,你也可以让它成为一个完整的函数。 @Timmmm 但是你将部分实现泄露给外界,通常 lambdas 与父函数紧密耦合(即使没有捕获)。如果不是这种情况,那么您不应该首先使用 lambda,而应使用函子的普通函数。【参考方案5】:要使 lambda 递归而不使用外部类和函数(如 std::function
或定点组合器),可以在 C++14 (live example) 中使用以下构造:
#include <utility>
#include <list>
#include <memory>
#include <iostream>
int main()
struct tree
int payload;
std::list< tree > children = ; // std::list of incomplete type is allowed
;
std::size_t indent = 0;
// indication of result type here is essential
const auto print = [&] (const auto & self, const tree & node) -> void
std::cout << std::string(indent, ' ') << node.payload << '\n';
++indent;
for (const tree & t : node.children)
self(self, t);
--indent;
;
print(print, 1, 2, 8, 3, 5, 7, 6, 4);
打印:
1
2
8
3
5
7
6
4
注意,lambda 的结果类型应该明确指定。
【讨论】:
这里唯一看起来有用的答案。 这实际上与将 lambda 本身作为参数传递相同。你怎么看不到@JohanLundberg 的帖子上面的帖子?【参考方案6】:您可以让 lambda 函数以递归方式调用自身。您唯一需要做的就是通过函数包装器引用它,以便编译器知道它的返回值和参数类型(您无法捕获尚未定义的变量——lambda 本身) .
function<int (int)> f;
f = [&f](int x)
if (x == 0) return 0;
return x + f(x-1);
;
printf("%d\n", f(10));
小心不要超出包装器 f 的范围。
【讨论】:
但是,这与接受的答案相同,并且可能会因使用 std 函数而受到惩罚。【参考方案7】:我使用std::function<>
捕获方法运行了一个比较递归函数与递归 lambda 函数的基准测试。在 clang 版本 4.1 上启用全面优化后,lambda 版本的运行速度明显变慢。
#include <iostream>
#include <functional>
#include <chrono>
uint64_t sum1(int n)
return (n <= 1) ? 1 : n + sum1(n - 1);
std::function<uint64_t(int)> sum2 = [&] (int n)
return (n <= 1) ? 1 : n + sum2(n - 1);
;
auto const ITERATIONS = 10000;
auto const DEPTH = 100000;
template <class Func, class Input>
void benchmark(Func&& func, Input&& input)
auto t1 = std::chrono::high_resolution_clock::now();
for (auto i = 0; i != ITERATIONS; ++i)
func(input);
auto t2 = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2-t1).count();
std::cout << "Duration: " << duration << std::endl;
int main()
benchmark(sum1, DEPTH);
benchmark(sum2, DEPTH);
产生结果:
Duration: 0 // regular function
Duration: 4027 // lambda function
(注意:我还确认了一个从 cin 获取输入的版本,以消除编译时评估)
Clang 还会产生编译器警告:
main.cc:10:29: warning: variable 'sum2' is uninitialized when used within its own initialization [-Wuninitialized]
这是预期的,也是安全的,但应该注意。
很高兴在我们的工具带中提供解决方案,但我认为如果性能要与当前方法相媲美,该语言将需要一种更好的方法来处理这种情况。
注意:
正如评论者所指出的,最新版本的 VC++ 似乎已经找到了一种将其优化到同等性能的方法。毕竟,也许我们不需要更好的方法来处理这个问题(语法糖除外)。
此外,正如最近几周其他一些 SO 帖子所概述的那样,std::function<>
本身的性能可能是导致速度变慢的原因而不是直接调用函数,至少当 lambda 捕获太大而无法适应某些库优化时空间std::function
用于小型函子(我猜有点像各种短字符串优化?)。
【讨论】:
-1。请注意,“lambda”版本需要更长时间的唯一原因是因为您将它绑定到 std::function,这使得 operator() 调用虚拟调用,这显然需要更长的时间。最重要的是,在 VS2012 发布模式下,您的代码在两种情况下花费的时间大致相同。 @YamMarcovic 什么?这是目前唯一已知的编写递归 lambda 的方法(这就是示例的重点)。我很高兴知道 VS2012 已经找到了一种优化这个用例的方法(尽管最近在这个主题上有更多的发展,显然如果我的 lambda 捕获了更多它就不适合 std::function small-内存函子优化或诸如此类)。 已确认。我误解了你的帖子。然后+1。 Gah,只有在您编辑此答案时才能投票。那么您能否再强调一下,比如在评论中? @YamMarcovic 完成。感谢您愿意提供反馈并在需要时对其进行改进。 +1 给你,好先生。 0 时间通常意味着“整个操作被优化掉了”。如果编译器证明你对计算结果没有做任何事情,那么从 cin 获取输入什么也做不了。【参考方案8】:这是基于@Barry 提出的 Y-combinator 解决方案的改进版本。
template <class F>
struct recursive
F f;
template <class... Ts>
decltype(auto) operator()(Ts&&... ts) const return f(std::ref(*this), std::forward<Ts>(ts)...);
template <class... Ts>
decltype(auto) operator()(Ts&&... ts) return f(std::ref(*this), std::forward<Ts>(ts)...);
;
template <class F> recursive(F) -> recursive<F>;
auto const rec = [](auto f) return recursivestd::move(f); ;
要使用它,可以执行以下操作
auto fib = rec([&](auto&& fib, int i)
// implementation detail omitted.
);
它类似于OCaml中的let rec
关键字,虽然不一样。
【讨论】:
这个答案至少需要 C++17。否则我会得到:error: expected constructor, destructor, or type conversion before ‘;’ token
in line template <class F> recursive(F) -> recursive<F>;
。【参考方案9】:
这是定点运算符的一个稍微简单的实现,这使得它更清楚到底发生了什么。
#include <iostream>
#include <functional>
using namespace std;
template<typename T, typename... Args>
struct fixpoint
typedef function<T(Args...)> effective_type;
typedef function<T(const effective_type&, Args...)> function_type;
function_type f_nonr;
T operator()(Args... args) const
return f_nonr(*this, args...);
fixpoint(const function_type& p_f)
: f_nonr(p_f)
;
int main()
auto fib_nonr = [](const function<int(int)>& f, int n) -> int
return n < 2 ? n : f(n-1) + f(n-2);
;
auto fib = fixpoint<int,int>(fib_nonr);
for (int i = 0; i < 6; ++i)
cout << fib(i) << '\n';
【讨论】:
我认为如果您将std::function
替换为函数指针(在内核中,它仅适用于普通函数和无状态 lambda),您可以改进您的答案(性能方面)。顺便说一句,fib_nonr
应该接受fixpoint<int,int>
,如果你使用std::function
,它需要从*this
创建新副本。【参考方案10】:
C++ 14: 这是一个递归匿名无状态/不捕获通用 lambda 集 输出 1, 20 中的所有数字
([](auto f, auto n, auto m)
f(f, n, m);
)(
[](auto f, auto n, auto m) -> void
cout << typeid(n).name() << el;
cout << n << el;
if (n<m)
f(f, ++n, m);
,
1, 20);
如果我理解正确,这是使用 Y-combinator 解决方案
这里是 sum(n, m) 版本
auto sum = [](auto n, auto m)
return ([](auto f, auto n, auto m)
int res = f(f, n, m);
return res;
)(
[](auto f, auto n, auto m) -> int
if (n > m)
return 0;
else
int sum = n + f(f, n + 1, m);
return sum;
,
n, m); ;
auto result = sum(1, 10); //result == 55
【讨论】:
【参考方案11】:您正在尝试捕获正在定义的变量(总和)。这可不好。
我认为真正的自递归 C++0x lambda 是不可能的。不过,您应该能够捕获其他 lambda。
【讨论】:
但如果 sum 的声明从 'auto' 更改为 std::function这是 OP 的最终答案。无论如何,Visual Studio 2010 不支持捕获全局变量。而且您不需要捕获它们,因为全局变量可以通过定义全局访问。以下答案改为使用局部变量。
#include <functional>
#include <iostream>
template<typename T>
struct t2t
typedef T t;
;
template<typename R, typename V1, typename V2>
struct fixpoint
typedef std::function<R (V1, V2)> func_t;
typedef std::function<func_t (func_t)> tfunc_t;
typedef std::function<func_t (tfunc_t)> yfunc_t;
class loopfunc_t
public:
func_t operator()(loopfunc_t v)const
return func(v);
template<typename L>
loopfunc_t(const L &l):func(l)
typedef V1 Parameter1_t;
typedef V2 Parameter2_t;
private:
std::function<func_t (loopfunc_t)> func;
;
static yfunc_t fix;
;
template<typename R, typename V1, typename V2>
typename fixpoint<R, V1, V2>::yfunc_t fixpoint<R, V1, V2>::fix = [](tfunc_t f) -> func_t
return [f](fixpoint<R, V1, V2>::loopfunc_t x) return f(x(x));
([f](fixpoint<R, V1, V2>::loopfunc_t x) -> fixpoint<R, V1, V2>::func_t
auto &ff = f;
return [ff, x](t2t<decltype(x)>::t::Parameter1_t v1,
t2t<decltype(x)>::t::Parameter1_t v2)
return ff(x(x))(v1, v2);
;
);
;
int _tmain(int argc, _TCHAR* argv[])
auto term = [](int a)->int
return a*a;
;
auto next = [](int a)->int
return ++a;
;
auto sum = fixpoint<int, int, int>::fix(
[term,next](std::function<int (int, int)> sum1) -> std::function<int (int, int)>
auto &term1 = term;
auto &next1 = next;
return [term1, next1, sum1](int a, int b)mutable ->int
if(a>b)
return 0;
else
return term1(a) + sum1(next1(a),b);
;
);
std::cout<<sum(1,10)<<std::endl; //385
return 0;
【讨论】:
是否有可能使这个答案编译器不可知?【参考方案13】:这个答案不如 Yankes 的答案,但还是这样:
using dp_type = void (*)();
using fp_type = void (*)(dp_type, unsigned, unsigned);
fp_type fp = [](dp_type dp, unsigned const a, unsigned const b)
::std::cout << a << ::std::endl;
return reinterpret_cast<fp_type>(dp)(dp, b, a + b);
;
fp(reinterpret_cast<dp_type>(fp), 0, 1);
【讨论】:
我认为你应该避免使用reinterpret_cast
。在您的情况下,可能最好的方法是创建一些替换dp_type
的结构。它应该有字段fp_type
,可以从fp_type
构造,并有运算符()
和fp_type
之类的参数。这将接近std::function
,但允许自引用参数。
我想发布一个没有结构的最小示例,请随时编辑我的答案并提供更完整的解决方案。 struct
还会增加一个额外的间接级别。该示例有效,并且演员表符合标准,我不知道-1
的用途。
不,struct 只能用作指针的容器,并将作为值传递。这不会比指针更多的间接或开销。还有关于-1
我不知道是谁给你的,但我认为这是因为reinterpret_cast
应该作为最后的手段。
cast
应该保证在 c++11 标准下工作。在我看来,使用 struct
可能会打败 lambda 对象的使用。毕竟,您提出的struct
是一个仿函数,利用了一个 lambda 对象。
查看@Pseudonym 解决方案,仅删除std::function
,您将得到与我的想法相近的东西。这可能与您的解决方案具有相似的性能。【参考方案14】:
您需要一个定点组合器。见this。
或者看下面的代码:
//As decltype(variable)::member_name is invalid currently,
//the following template is a workaround.
//Usage: t2t<decltype(variable)>::t::member_name
template<typename T>
struct t2t
typedef T t;
;
template<typename R, typename V>
struct fixpoint
typedef std::function<R (V)> func_t;
typedef std::function<func_t (func_t)> tfunc_t;
typedef std::function<func_t (tfunc_t)> yfunc_t;
class loopfunc_t
public:
func_t operator()(loopfunc_t v)const
return func(v);
template<typename L>
loopfunc_t(const L &l):func(l)
typedef V Parameter_t;
private:
std::function<func_t (loopfunc_t)> func;
;
static yfunc_t fix;
;
template<typename R, typename V>
typename fixpoint<R, V>::yfunc_t fixpoint<R, V>::fix =
[](fixpoint<R, V>::tfunc_t f) -> fixpoint<R, V>::func_t
fixpoint<R, V>::loopfunc_t l = [f](fixpoint<R, V>::loopfunc_t x) ->
fixpoint<R, V>::func_t
//f cannot be captured since it is not a local variable
//of this scope. We need a new reference to it.
auto &ff = f;
//We need struct t2t because template parameter
//V is not accessable in this level.
return [ff, x](t2t<decltype(x)>::t::Parameter_t v)
return ff(x(x))(v);
;
;
return l(l);
;
int _tmain(int argc, _TCHAR* argv[])
int v = 0;
std::function<int (int)> fac =
fixpoint<int, int>::fix([](std::function<int (int)> f)
-> std::function<int (int)>
return [f](int i) -> int
if(i==0) return 1;
else return i * f(i-1);
;
);
int i = fac(10);
std::cout << i; //3628800
return 0;
【讨论】:
以上是关于C++11 中的递归 lambda 函数的主要内容,如果未能解决你的问题,请参考以下文章
Python:lambda表达式和yield关键字理解与使用讲解
为啥 c++11 中的 lambda 函数没有 function<> 类型?