Lambdas和引用捕获局部变量:在范围之后访问

Lambdas and capture by reference local variables : Accessing after the scope

本文关键字:范围 之后 访问 局部变量 引用 Lambdas      更新时间:2023-10-16

我通过引用两个lambda来传递我的局部变量。我在函数范围之外调用这些 lambda。这是undefined吗?

std::pair<std::function<int()>, std::function<int()>> addSome() {
    int a = 0, b = 0;
    return std::make_pair([&a,&b] {
        ++a; ++b;
        return a+b;
        }, [&a, &b] {
            return a;
        });
}
int main() {
    auto f = addSome();
    std::cout << f.first() << " " << f.second();
    return 0;
}

但是,如果不是,则一个 lambda 中的更改不会反映在其他 lambda 中。

我是否误解了 lambda 上下文中的按引用传递?

我正在写入变量,它似乎工作正常,输出没有运行时错误

2 0 .如果它有效,那么我希望输出2 1.

是的,这会导致未定义的行为。lambda 将引用超出范围的堆栈分配对象。(从技术上讲,据我了解,该行为是在 lambda 访问a和/或b之前定义的。如果您从未调用返回的 lambda,则没有 UB。

这是未定义的行为,就像

返回对堆栈分配的本地的引用,然后在本地超出范围后使用该引用的未定义行为相同,只是在这种情况下,lambda 对其进行了一点混淆。

此外,请注意,调用 lambda 的顺序是未指定的 - 编译器可以在f.first()之前自由调用 f.second(),因为两者都是同一完整表达式的一部分。 因此,即使我们修复了因使用对已销毁对象的引用而导致的未定义行为,2 02 1 仍然是该程序的有效输出,并且您获得的输出取决于编译器决定执行 lambda 的顺序。 请注意,这不是未定义的行为,因为编译器根本无法执行任何操作,而只是在决定执行某些操作的顺序方面具有一些自由。

(请记住,main()函数中的<<是调用自定义operator<<函数,并且未指定函数参数的计算顺序。编译器可以自由地发出代码,以任何顺序计算同一完整表达式中的所有函数参数,但约束是在调用函数之前必须计算函数的所有参数。

若要解决第一个问题,请使用 std::shared_ptr 创建引用计数的对象。 按值捕获此共享指针,只要指向的对象(及其任何副本)存在,lambda 就会使指向的对象保持活动状态。 这个堆分配的对象是我们将存储共享状态的地方 ab .

要解决第二个问题,请在单独的语句中评估每个 lambda。

以下是使用未定义的行为重写的代码,并保证在f.second()之前调用f.first()

std::pair<std::function<int()>, std::function<int()>> addSome() {
    // We store the "a" and "b" ints instead in a shared_ptr containing a pair.
    auto numbers = std::make_shared<std::pair<int, int>>(0, 0);
    // a becomes numbers->first
    // b becomes numbers->second
    // And we capture the shared_ptr by value.
    return std::make_pair(
        [numbers] {
            ++numbers->first;
            ++numbers->second;
            return numbers->first + numbers->second;
        },
        [numbers] {
            return numbers->first;
        }
    );
}
int main() {
    auto f = addSome();
    // We break apart the output into two statements to guarantee that f.first()
    // is evaluated prior to f.second().
    std::cout << f.first();
    std::cout << " " << f.second();
    return 0;
}

(看它运行。

不幸的是C++lambdas可以通过引用捕获,但不能解决"向上funarg问题"。

这样做需要在"单元格"中分配捕获的局部变量,并进行垃圾回收或引用计数以进行释放。C++没有这样做,不幸的是,这使得C++lambdas比其他语言(如Lisp,Python或Javascript)更不有用,更危险。

更具体地说,根据我的经验,您应该不惜一切代价避免通过引用(即使用 [&](…){…} 形式)对在本地范围内幸存下来的 lambda 对象进行隐式捕获,因为这是以后维护期间随机段错误的秘诀。

始终仔细规划要捕获的内容、捕获方式和捕获引用的生存期。

当然,如果您所做的只是在同一范围内使用 lambda 将代码传递给std::sort等算法,而无需在函数外部定义命名比较器函数或作为本地使用的实用程序函数,那么通过引用[&]捕获所有内容是安全的(我发现这种用法非常可读且很好,因为您可以隐式获取大量上下文并且没有需要 1.为永远不会在其他任何地方重复使用的东西组成一个全局名称, 2.传递大量上下文或仅为该上下文创建额外的类)。

有时可以奏效的方法是按捕获堆分配状态shared_ptr。这基本上是手动实现Python自动执行的操作(但请注意引用周期以避免内存泄漏:Python有一个垃圾收集器,C++没有)。

当您超出范围时,请复制您用于按值捕获的局部变量 ( [=] ):

MyType func(void)
{
    int x = 5;
    //When called, local x will no longer be in scope; so, use capture by value.
    return ([=] {
         x += 2;
    });
}

当您在同一范围内时,最好使用引用捕获([&]):

void func(void)
{
    int x = 5;
    //When called, local x will still be in scope; safe to use capture by reference.
    ([&] {
        x += 2;
    })(); //Lambda is immediately invoked here, in the same scope as x, with ().
}