我们可以定义像
这样的递归lambda函数std::function<void(int)> fun = [&fun](int a) { if (a) fun(a - 1); };
然后我们可以用
来调用它fun(10);
但是,如果我将定义更改为
std::function<void(int)> fun = [fun](int a) { if (a) fun(a - 1); };
然后尝试使用
进行调用fun(10);
发生分段错误。
有人可以解释为什么通过引用捕获工作而按值捕获会产生分段错误。
答案 0 :(得分:8)
按值计算,作为评估lambda表达式的一部分进行评估。那时,fun
仍未被初始化,因为您仍在评估其初始化。仅在此之后fun
被初始化,但到那时复制已经发生。
净效应是存储在fun
内的lambda函数对象有一个名为fun
的数据成员,它是未经初始化的std::function
- 未定义行为的副本。
答案 1 :(得分:1)
将std::function
用于递归lambda不是一个好计划。在你的情况下,你得到一个未初始化的函数副本,然后在其中包含lambda。
哪个好看。当你幸运时,未定义的行为会崩溃。
递归Lambdas
假设我们希望将Euclid的gcd()
写为lambda。作为一个功能,它是:
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a%b);
}
但是lambda不能递归,它无法调用自身。 lambda没有名称,并且在lambda的主体内使用this
指的是捕获的this
(假设lambda是在成员函数的主体中创建的,否则它是一个错误)。那么我们如何解决这个问题呢?
std::function
我们可以让lambda捕获对尚未构造的std::function
的引用:
std::function<int(int, int)> gcd = [&](int a, int b){
return b == 0 ? a : gcd(b, a%b);
};
这有效,但应谨慎使用。它很慢(我们现在使用类型擦除而不是直接函数调用),它很脆弱(复制gcd
或返回gcd
会因为lambda引用原始对象而中断),它赢了使用通用lambdas。
auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>();
*gcd_self = std::make_unique<std::function<int(int, int)>>(
[gcd_self](int a, int b){
return b == 0 ? a : (**gcd_self)(b, a%b);
};
};
这增加了很多间接(这是开销),但可以复制/返回,并且所有副本共享状态。它确实让你返回lambda,否则不如上面的解决方案那么脆弱。
借助简短的实用程序结构,我们可以解决所有这些问题:
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.
// the lambda should take the first argument as `auto&& recurse` or similar.
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)};
}
// (Be aware that in C++17 we can do better than a `make_` function)
我们可以将gcd
实现为:
auto gcd = make_y_combinator(
[](auto&& gcd, int a, int b){
return b == 0 ? a : gcd(b, a%b);
}
);
y_combinator
是lambda演算中的一个概念,它允许您在定义之前无法为自己命名。这正是lambda所具有的问题。
您创建一个以“recurse”作为其第一个参数的lambda。当你想要递归时,你传递参数来递归。
y_combinator
然后返回一个函数对象,该函数对象使用其参数调用该函数,但使用合适的“recurse”对象(即y_combinator
本身)作为其第一个参数。它将你称为y_combinator
的其余参数转发给lambda。
简而言之:
auto foo = make_y_combinator( [&](auto&& recurse, some arguments) {
// write body that processes some arguments
// when you want to recurse, call recurse(some other arguments)
});
并且您在lambda中进行递归,没有严重的限制或显着的开销。
这个答案的一部分(The Recursive Lambda)最初由@Barry写在已解散的Stack Overflow Documentation上。