C++ lambda函数是真正的闭包吗?引用捕获

8
在下面的代码中,我创建了一个通过引用捕获本地变量的lambda。请注意,它是一个指针,因此,如果C++ lambda是真正的闭包,它应该在创建lambda的函数的生命周期内存活。
然而,当我再次调用它时,它不会创建新的本地变量(新环境),而是重复使用先前的变量,并且实际上捕获了与之前完全相同的指针。
这似乎是错误的。要么C++ lambda不是真正的闭包,要么我的代码有误?
感谢您的帮助。
#include <iostream>
#include <functional>
#include <memory>

std::function<int()> create_counter()
{
    std::shared_ptr<int> counter = std::make_shared<int>(0);

    auto f = [&] () -> int { return ++(*counter); };

    return f;
}


int main()
{
   auto counter1 = create_counter();
   auto counter2 = create_counter();

   std::cout << counter1() << std::endl;
   std::cout << counter1() << std::endl;

   std::cout << counter2() << std::endl;
   std::cout << counter2() << std::endl;

   std::cout << counter1() << std::endl;


   return 0;
}

这段代码返回:
1
2
3
4
5

但是我期望它会返回:
1
2
1
2
3

进一步编辑:

感谢您指出我原始代码中的错误。现在我明白了,发生的情况是在调用create_couter后指针被删除,新的create只是重复使用同一块内存地址。

那么这就带来了我真正的问题,我想要做的是:

std::function<int()> create_counter()
{
    int counter = 0;

    auto f = [&] () -> int { return ++counter; };

    return f;
}

如果 C++ Lambda 是真正的闭包,那么每个局部计数器将与返回的函数共存(该函数携带其环境——至少是部分环境)。但事实上,调用 create_counter 后计数器被销毁,调用返回的函数会导致段错误。这不是闭包的预期行为。
Marco A 建议一种解决办法:通过复制传递指针。这增加了引用计数器,因此在 create_counter 后它不会被销毁。但这只是一个折中办法。但正如 Marco 指出的那样,它能够正常工作并且完全符合我的期望。
Jarod42 建议声明变量并将其初始化为捕获列表的一部分。但这破坏了闭包的目的,因为这些变量现在是函数局部变量,而不是创建函数时的环境变量。
apple apple 建议使用静态计数器。但这是一种避免在 create_function 结束时销毁变量的解决办法,并且意味着所有返回的函数共享同一个变量,而不是运行环境下的环境变量。
因此,我猜结论是(除非有人能提供更多信息)C++ 中的 Lambda 不是真正的闭包。
感谢您的评论。

6
请注意,它是一个指针,因此它的生命周期与创建lambda函数的函数相同。但实际上并不是这样的。 - T.C.
6
这段代码依赖于一个悬空引用,因此存在未定义行为。 - Marco A.
3
你的代码有误,而且(实际上)是因为lambda函数不是真正的闭包。 - n. m.
1
你的问题存在“无真苏格兰人谬误”——C++中的闭包是(或可以是)真正的闭包,但这并不意味着引用变量的生命周期被延长。换句话说,“真正的闭包”不是一个明确定义的术语。 - Remember Monica
5个回答

12
在函数范围结束时,共享指针被销毁并释放内存:您正在存储一个悬空引用。
std::function<int()> create_counter()
{
  std::shared_ptr<int> counter = std::make_shared<int>(0);

  auto f = [&]() -> int { return ++(*counter); };

  return f;
} // counter gets destroyed

因此调用 未定义行为。通过将整数替换为类或结构体并检查析构函数是否被调用来自行测试。
方式捕获会增加共享指针的使用计数器并防止该问题发生。
auto f = [=]() -> int { return ++(*counter); };
          ^

似乎这修复了我的代码中的错误...但是...一个真正的闭包将允许我这样说:auto f = [&] int {return counter ++;};(假设counter 是一个 int 类型的局部变量)。但是,如果我这么做,我的代码会崩溃,因为我认为--局部变量--counter在函数 create_counter 结束时被删除。但是,如果我将它声明为 [=],那么我就不能对它进行递增操作。所以,指针是一种使其感觉像闭包的笨拙方法。 - dmg
2
@dmg 你可以在这里阅读更多关于lambda函数的内容:有多种捕获变量的方式,并且通过引用捕获一个变量之间存在巨大的差异。通过值捕获将会得到您期望的共享指针结果。请查看此代码示例 - Marco A.
@MarcoA。为什么要说“可能”会调用未定义的行为?它是一个典型的例子。 - The Vee
因为它甚至在我的系统上都无法编译.. :) - Marco A.
@MarcoA. 嗯,那是 UB 在法律上可以做的范畴内 :-) 只是想知道,错误信息是什么? - The Vee
2
@TheVee,结果我发现我的流水线是人为设置的(在中间进行了清洗),这使得我根本无法编译它。如果不进行清洗,它就能编译。 - Marco A.

5

如上所述,你有一个悬空引用,因为局部变量在作用域结束时被销毁。

你可以简化你的函数为

std::function<int()> create_counter()
{
    int counter = 0;

    return [=] () mutable -> int { return ++counter; };
}

甚至可以(在C++14中)
auto create_counter()
{
    return [counter = 0] () mutable -> int { return ++counter; };
}

Demo


2
Lambda表达式- Lambda表达式指定内联指定的对象,不仅是没有名称的函数,还能够捕获作用域中的变量。
  1. Lambdas可以频繁地作为对象传递。
  2. 除了它自己的函数参数外,Lambda表达式也可以引用定义作用域中的局部变量。
闭包-
闭包是一种特殊的函数,可以捕获环境,即词法作用域中的变量。

闭包是任何在其定义的环境中关闭的函数。这意味着它可以访问不在其参数列表中的变量。

  • C++在这里有什么特殊之处 闭包是编程中从函数式编程起源的一般概念。当我们谈论C++中的闭包时,它们总是与lambda表达式一起出现(一些学者喜欢将函数对象纳入其中)
在C ++中,lambda表达式是用于创建类似于函数对象行为的特殊临时对象的语法。

C++标准专门将这种类型的对象称为闭包对象。 这与更广泛的闭包定义略有不同,后者指的是从定义它们的环境中捕获变量的任何函数,无论是否匿名。

就标准而言,Lambda表达式的所有实例都是闭包对象,即使它们在捕获组中没有任何捕获。
来源:https://pranayaggarwal25.medium.com/lambdas-closures-c-d5f16211de9a

0
如果变量被值捕获,那么它将从原始变量进行复制构造。如果是通过引用,则可以将它们视为对同一对象的不同引用。

0
如果你想要“1 2 3 4 5”,你也可以尝试这个。
std::function<int()> create_counter()
{
    static int counter = 0;

    auto f = [&] () -> int { return ++counter; };

    return f;
}

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