21

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

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

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

4

2 回答 2

24

因为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. 您应该简单地按值捕获,然后仍然int &p在 lambda 内部创建您的,以获得您想要的语法。

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 有一个扩展,即使有多个语句也允许返回类型推导,但你至少应该得到一个警告。)

于 2012-05-20T01:33:08.757 回答
1

这是我的测试代码。它使用递归函数将 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 参数。

于 2013-06-03T06:13:19.060 回答