C++ 实现lambda递归调用(C++11 - C++23)

本文介绍了C++11到C++23中使用lambda表达式进行递归调用的多种方法,包括借助std::function、Y不动点组合子、科里化、直接传入、打包以及利用C++23的Deducing this特性。通过示例代码展示了如何实现斐波那契数列的递归计算,讨论了返回值推断问题,并探讨了不同方法的优缺点。

前言

众所周知,C++11起出现了 lambda表达式 ,或者叫闭包,使得C++函数式编程的实现更为容易。本文不对lambda表达式的历史意义以及工程意义做过多探讨,只是给大家介绍一下如何递归调用lambda表达式。关于lambda表达式的基本用法,敬请移步 cppreference或相关博客,本文默认读者具有最基本的lambda语法认识。

我们知道,倘若直接如同普通函数一般在函数体内尝试递归调用是会语法错误的,无论是采用引用捕获亦或是值捕获都无法完成,此处不做赘述。因此我们需要借助别的东西来实现递归调用。
在这里插入图片描述

前置知识

阅读本文,你需要有:

  • 相当基本的C++语法基础
  • 十分基本的C++ lambda表达式语法基础
  • 非常基本的递归函数认识
  • 最为基本的中文基础

C++11,借助std::function

以斐波那契数列为例,我们可以使用lambda表达式来构造一个 std::function 对象,如同这样:

#include <iostream>
#include <functional>
int main(int argc, char* argv[])
{
	std::function<int(int)> fib = [&fib](int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); };
	std::cout << fib(5);
	return 0;
}

在这里插入图片描述
不过很显然,这种方法从声明形式上来看并不是那么优雅,从书写形式上来看,右边lambda写了一遍的函数签名左边还要照抄一遍,过于繁琐与丑陋。此外,用闭包去初始化std::function对象,本质上并没有解决lambda递归调用的问题,只是规避了这个问题而已,反而引入了许多新的问题,它并不是零开销抽象的。另外他还有一些功能上的残缺,比如我们尝试实现一个尾递归调用的斐波那契数列:

在这里插入图片描述
很显然,对于带有默认参数的lambda表达式,std::function并不能承载其全部功能。

C++14,基于Y不动点组合子(Y Combinator)

如果读者有基本的lambda演算基础,应该对于“Y不动点组合子”有一些概念,它是用来解决匿名函数的递归调用问题的。

科里化

具体理论此处不做讲述,直接看代码,相对纯正的FP写法如下,由于我们要做科里化,传递lambda表达式,此处使用了来自C++14的泛型lambda,即支持在lambda表达式的参数列表中使用auto:

#include <iostream>

int main() {
    auto T = [&](auto x) { return [&](auto y) { return y([&](auto z) {return x(x)(y)(z); }); }; };
    auto X = T(T);
    auto fib = X([](auto f) { return [&](auto n)->int { return n < 2 ? n : f(n - 1) + f(n - 2); }; });
    std::cout << fib(5);
    return 0;
}

在这里插入图片描述

直接传入

这样看起来比较繁琐,我们可以这样简化:

#include <iostream>
int main(int argc, char* argv[])
{
	auto fib = [](auto&& self, int n, int i = 0, int num1 = 0, int num2 = 1) {
		if (i >= n) return num1;
		else return self(self, n, i + 1, num2, num1 + num2);
	};
	std::cout << fib(fib, 5);
	return 0;
}

在这里插入图片描述

打包

区别于使用科里化构造一个构造器,我们这里直接选择接受一个lambda表达式作为参数调用,缺点是每次调用的时候需要将自己传进去,因此我们直接使用另一个lambda或者std::bind打包一下就行了:

#include <iostream>
int main(int argc, char* argv[])
{
	auto f = [](auto&& self, int n, int i = 0, int num1 = 0, int num2 = 1) {
		if (i >= n) return num1;
		else return self(self, n, i + 1, num2, num1 + num2);
	};
	auto fib = [&f](int n) { return f(f, n); };
	std::cout << fib(5);
	return 0;
}

在这里插入图片描述

不过这样实际上有一点不好,就是我们只需要一个fib,但是却多出来了一个f,污染了命名空间,那么如何解决呢?比较容易想到的是直接定义在fib的函数体内部:

#include <iostream>
int main(int argc, char* argv[])
{
	auto fib = [](int n) {
		auto f = [](auto&& self, int n, int i = 0, int num1 = 0, int num2 = 1) {
			if (i >= n) return num1;
			else return self(self, n, i + 1, num2, num1 + num2); 
		};
		return f(f, n); 
	};
	std::cout << fib(5);
	return 0;
}

在这里插入图片描述

但是这样很显然,我们每调用一次fib,都要重新初始化一次f,如果不考虑编译器优化,将会有肉眼可见的性能开销,因此在这里我们可以使用同样来自C++14的带初始化器的捕获列表,这样f将只会在fib初始化时初始化:

#include <iostream>
int main(int argc, char* argv[])
{
	auto fib = [
		f = [](auto&& self, int n, int i = 0, int num1 = 0, int num2 = 1) {
				if (i >= n) return num1;
				else return self(self, n, i + 1, num2, num1 + num2);
		}](int n) { return f(f, n); };;
	std::cout << fib(5);
	return 0;
}

在这里插入图片描述

关于返回值推断

在最后,需要指出的一点是,注意到我在最开始是直接返回的一个三目运算符,而在此处则使用的if - else语句返回,这是为了引出一个问题,即自动推断的返回值问题。
我们将代码改为这样,可以发现发生了一个语法错误:

#include <iostream>
int main(int argc, char* argv[])
{
	auto fib = [
		f = [](auto&& self, int n, int i = 0, int num1 = 0, int num2 = 1) {
				return i >= n ? num1 : self(self, n, i + 1, num2, num1 + num2);
		}](int n) { return f(f, n); };
	std::cout << fib(5);
	return 0;
}

在这里插入图片描述
通过阅读报错可以知道,我们尝试调用self,但是我们并不知道self的返回类型。关于发生这个错误的原因,通俗来描述,就是在知道函数返回值之前便调用了这个函数。搞清楚了原因,解决起来也就很简单了,一种方法是如同上文那样,在调用前返回一个值,这样编译器就能提前推断出返回值,另一种是使用尾后返回值类型:

#include <iostream>
int main(int argc, char* argv[])
{
	auto fib = [
		f = [](auto&& self, int n, int i = 0, int num1 = 0, int num2 = 1)->int {
			return i >= n ? num1 : self(self, n, i + 1, num2, num1 + num2);
		}](int n) { return f(f, n); };
	std::cout << fib(5);
	return 0;
}

在这里插入图片描述

C++23 借助Deducing this实现lambda递归

实际上早在2017年的提案 p0839r0 就对于简化lambda递归做了努力,他尝试这么做:

#include <iostream>
int main(int argc, char* argv[])
{
	auto fib = [] self(int n) {
		if (n < 2) return n;
		return self(n - 1) + self(n - 2);
	};
	std::cout << fib(5);
	return 0;
}

差不多是给lambda起个名字,然而并没有实装。。

C++23有这样一条提案:P0847R7,这条提案实际上并不是为了lambda准备的,但是lambda递归刚好可以利用上。详细内容可以自行查看,这里直接展示用法:
在这里插入图片描述
可以看到,基本语法是和上面Y不动点组合子的形式类似的,同样是第一个参数接受一个闭包对象,不过调用形式更为美观了。

借此我们可以直接一行爆栈(雾)
在这里插入图片描述

### C++ Lambda 表达式实现递归调用C++中,由于lambda表达式的匿名特性,默认情况下无法直接通过名称进行自我引用从而实现递归。然而,有几种方法可以绕过这个问题并成功实现递归。 #### 方法一:使用外部变量保存lambda表达式 一种常见的方式是将lambda赋值给一个自动类型的变量,然后在这个lambda内部使用该变量来进行递归调用: ```cpp #include <iostream> using namespace std; int main() { // 定义带有返回值和参数的lambda,并将其存储在一个auto类型的变量中 auto factorial = [&factorial](int n) -> int { return (n <= 1) ? 1 : (n * factorial(n - 1)); }; cout << "Factorial of 5 is " << factorial(5) << endl; } ``` 这种方法利用了`auto`关键字推导出合适的函数签名,并允许lambda在其体内访问自己作为闭包的一部分[^2]。 #### 方法二:Y组合子方式 另一种更复杂但也更为通用的方法是采用固定点组合子(Fixpoint Combinator),也称为Y组合子。这种方式不依赖于特定的语言特性和命名空间污染,而是基于纯逻辑构建递归机制。下面是一个简化版的例子: ```cpp #include <functional> template<typename F> struct RecursiveLambda { F f; }; // Helper function to create a recursive lambda. template<typename T> RecursiveLambda<T> make_recursive_lambda(T t) { static T* ptr; ptr = &t; return { [ptr](auto&&... args)->decltype(auto){ return (*ptr)(std::forward<decltype(args)>(args)...); }}; } int main(){ auto fib = make_recursive_lambda<int(*)(int)>( [](auto self, int n){ if (n<=1) return n; else return self(self,n-1)+self(self,n-2); } ).f; cout << "Fibonacci number at position 7 is " << fib(fib.f, 7) << endl; } ``` 此代码片段展示了如何创建一个能够递归调用自己的无名函数实例。这里的关键在于辅助结构体`RecursiveLambda`以及帮助函数`make_recursive_lambda`的设计,它们共同作用使得lambda能够在不知道自身名字的情况下完成递归操作[^4]。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值