【问题标题】:In C++11 lambda syntax, heap-allocated closures?在 C++11 lambda 语法中,堆分配的闭包?
【发布时间】:2012-05-20 01:31:36
【问题描述】:

C++11 lambda 很棒!

但是缺少一件事,那就是如何安全地处理可变数据。

以下将在第一次计数后给出错误计数:

#include <cstdio>
#include <functional>
#include <memory>

std::function<int(void)> f1()
{
    int k = 121;
    return std::function<int(void)>([&]{return k++;});
}

int main()
{
    int j = 50;
    auto g = f1();
    printf("%d\n", g());
    printf("%d\n", g());
    printf("%d\n", g());
    printf("%d\n", g());
}

给予,

$ g++-4.5 -std=c++0x -o test test.cpp && ./test
121
8365280
8365280
8365280

原因是f1() 返回后,k 超出范围但仍在堆栈中。所以第一次执行g() k 没问题,但之后堆栈损坏并且k 失去了它的价值。

因此,我设法在 C++11 中实现安全可返回闭包的唯一方法是在堆上显式分配已关闭变量:

std::function<int(void)> f2()
{
    int k = 121;
    std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
    return std::function<int(void)>([=]{return (*o)++;});
}

int main()
{
    int j = 50;
auto g = f2();
    printf("%d\n", g());
    printf("%d\n", g());
    printf("%d\n", g());
    printf("%d\n", g());
}

这里,[=] 用于确保共享指针被复制,而不是被引用,以便正确完成内存处理:k 的堆分配副本应该在生成的函数g 超出范围。结果如你所愿,

$ g++-4.5 -std=c++0x -o test test.cpp && ./test
121
122
123
124

通过解除引用来引用变量是相当丑陋的,但应该可以使用引用来代替:

std::function<int(void)> f3()
{
    int k = 121;
    std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
    int &p = *o;
    return std::function<int(void)>([&]{return p++;});
}

其实,这很奇怪,

$ g++-4.5 -std=c++0x -o test test.cpp && ./test
0
1
2
3

知道为什么吗?考虑到共享指针的引用可能不礼貌,因为它不是跟踪引用。我发现将引用移动到 lambda 内部会导致崩溃,

std::function<int(void)> f4()
{
    int k = 121;
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
    return std::function<int(void)>([&]{int &p = *o; return p++;});
}

给予,

g++-4.5 -std=c++0x -o test test.cpp && ./test
156565552
/bin/bash: line 1: 25219 Segmentation fault      ./test

无论如何,如果有一种方法可以通过堆分配自动创建安全可返回的闭包,那就太好了。例如,如果有一个替代 [=][&amp;] 的选项表明变量应该在堆上分配并通过对共享指针的引用来引用。当我了解std::function 时,我最初的想法是它创建了一个封装闭包的对象,因此它可以为闭包环境提供存储,但我的实验表明这似乎没有帮助。

我认为 C++11 中的安全可返回闭包对于使用它们至关重要,有谁知道如何更优雅地实现这一点?

【问题讨论】:

  • 您应该在 Valgrind 中运行这样的测试,因为您在访问释放的内存时会偶然看到正确的行为。

标签: c++ lambda c++11


【解决方案1】:

f1 中,由于您所说的原因,您得到了未定义的行为; lambda 包含对局部变量的引用,并且在函数返回后该引用不再有效。为了解决这个问题,您不必在堆上分配,您只需声明捕获的值是可变的:

int k = 121;
return std::function<int(void)>([=]() mutable {return k++;});

您必须小心使用此 lambda,因为它的不同副本将修改它们自己的捕获变量副本。通常算法期望使用仿函数的副本等同于使用原始的。我认为只有一种算法实际上允许有状态的函数对象,std::for_each,它返回它使用的函数对象的另一个副本,以便您可以访问发生的任何修改。


f3 中,没有任何东西维护共享指针的副本,因此正在释放内存并访问它会产生未定义的行为。您可以通过按值显式捕获共享指针并仍按引用捕获指向的 int 来解决此问题。

std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
int &p = *o;
return std::function<int(void)>([&p,o]{return p++;});

f4 再次是未定义的行为,因为您再次捕获对局部变量 o 的引用。您应该简单地按值捕获,然后仍然在 lambda 中创建您的 int &amp;p 以获得您想要的语法。

std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
return std::function<int(void)>([o]() -> int {int &p = *o; return p++;});

请注意,当您添加第二条语句时,C++11 不再允许您省略返回类型。 (clang 和我假设 gcc 有一个扩展,即使有多个语句也允许返回类型推导,但你至少应该得到一个警告。)

【讨论】:

  • 请注意,在 lambda 中访问状态比旧式仿函数更难。你唯一能做的就是再次调用它。但是有状态的 lambdas 仍然有用,即使它们不返回辅助结果。
  • "我认为只有一种算法实际上允许有状态的 lambda,std::accumulate,它返回它使用的 lambda,以便您可以访问发生的任何修改。" 你想的是std::for_each
  • 如果同一范围内的不同闭包实例获得了自己的变量副本,则不会捕获该变量!!!
  • @Kaz IIUC,你说[&amp;] lambdas 是正确的闭包,而[=] 不是。为什么?是不是因为其他语言通常定义引用(而不是复制)外部环境的闭包?
【解决方案2】:

这是我的测试代码。它使用递归函数将 lambda 参数的地址与其他基于堆栈的变量进行比较。

#include <stdio.h> 
#include <functional> 

void fun2( std::function<void()> callback ) { 
    (callback)(); 
} 

void fun1(int n) { 
    if(n <= 0) return; 
    printf("stack address = %p, ", &n); 

    fun2([n]() { 
        printf("capture address = %p\n", &n); 
        fun1(n - 1); 
    }); 
} 

int main() { 
    fun1(200); 
    return 0; 
}

用mingw64编译代码并在Win7上运行,输出

stack address = 000000000022F1E0, capture address = 00000000002F6D20
stack address = 000000000022F0C0, capture address = 00000000002F6D40
stack address = 000000000022EFA0, capture address = 00000000002F6D60
stack address = 000000000022EE80, capture address = 00000000002F6D80
stack address = 000000000022ED60, capture address = 00000000002F6DA0
stack address = 000000000022EC40, capture address = 00000000002F6DC0
stack address = 000000000022EB20, capture address = 00000000007A7810
stack address = 000000000022EA00, capture address = 00000000007A7820
stack address = 000000000022E8E0, capture address = 00000000007A7830
stack address = 000000000022E7C0, capture address = 00000000007A7840

很明显,捕获的参数不在堆栈区, 并且捕获参数的地址不连续

所以我相信有些编译器可能会使用动态内存分配
捕获 lambda 参数。

【讨论】:

  • Lambda 对象只是用于创建普通函数对象的语法糖。捕获的变量成为函数对象的成员。除非您专门编写代码来执行此操作,否则不会使用动态内存分配。
  • @bames53 根据这篇文章:drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/…,std::function 实现实际上是允许做动态分配的。
  • @brightstar 我只是在谈论编译器对 lambda 对象的实现。上面的答案错误地将std::function&lt;&gt; 实现中的动态分配归因于编译器对 lambda 对象的实现。
  • 谢谢你,笨蛋!我终于意识到它是 std::function 动态分配内存,而不是 lambda。
猜你喜欢
  • 1970-01-01
  • 2016-04-25
  • 1970-01-01
  • 2019-10-17
  • 2016-06-16
  • 1970-01-01
  • 2013-05-10
  • 2020-05-01
  • 1970-01-01
相关资源
最近更新 更多