在 C++11 lambda 语法中,堆分配的闭包

In C++11 lambda syntax, heap-allocated closures?

本文关键字:分配 闭包 C++11 lambda 语法      更新时间:2023-10-16

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("%dn", g());
    printf("%dn", g());
    printf("%dn", g());
    printf("%dn", 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("%dn", g());
    printf("%dn", g());
    printf("%dn", g());
    printf("%dn", 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

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

我认为 C++11 中的安全可回收闭合对于使用它们至关重要,有谁知道如何更优雅地完成此操作?

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 &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 参数的地址与其他基于堆栈的变量进行比较。

#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 = %pn", &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 参数。