C++11中使用lambda表达式出现的非确定性损坏问题

6
受Herb Sutter的精彩演讲Not your father's C ++的启发,我决定使用Microsoft的Visual Studio 2010再次查看最新版本的C ++。我特别感兴趣的是Herb的断言,“C ++是安全的”,因为我没有听说C ++ 11如何解决已知的向上funarg问题。据我所知,C ++ 11对解决此问题毫无作为,因此不“安全”。
您不想返回对局部变量的引用,因为该局部变量分配在堆栈帧上,在函数返回后将不存在,因此函数将返回指向已分配内存的悬空指针,这可能会导致非确定性数据损坏。 C和C ++编译器知道这一点,并且如果您尝试返回对局部变量的引用或指针,则会发出警告。例如,此程序:
int &bar() {
  int n=0;
  return n;
}

导致 Visual Studio 2010 发出警告:

warning C4172: returning address of local variable or temporary

然而,在C++11中,lambda表达式可以轻松地通过引用捕获本地变量并返回该引用,导致等效的悬空指针。考虑以下函数foo,它返回一个捕获本地变量n并返回它的lambda函数:
#include <functional>

std::function<int()> foo(int n) {
  return [&](){return n;};
}

这个看起来无害的函数存在内存不安全问题,会导致数据损坏。在一个地方调用该函数以获取lambda,然后在另一个地方调用lambda并打印其返回值,会得到以下输出结果:
1825836376

此外,Visual Studio 2010没有任何警告。
对我来说,这看起来像是语言中非常严重的设计缺陷。即使是最简单的重构也可能使lambda跨越堆栈帧,悄悄地引入不确定性数据损坏。然而,关于这个问题似乎很少有信息(例如,在StackOverflow上搜索“upwards funarg”和C++没有任何命中)。人们是否知道这一点?有人正在解决方案或描述解决方法吗?

2
我在这里看到的唯一问题是缺乏警告,但这并不完全是语言设计上的缺陷。 - Michael Krelin - hacker
1
我对此没有解决方案,但我猜这只是另一种使用C/C++真正伤害自己的方式 - “伴随着强大的力量而来的是巨大的责任” ;D - Random Dev
1
@CarstenKönig,感谢OP的提醒,有备无患 :) - Michael Krelin - hacker
3
是的,人们已经意识到这一点。(例如,请参考http://blogs.msdn.com/b/nativeconcurrency/archive/2012/01/29/perils-of-lambda-capture.aspx)。这并不比允许你返回本地引用而没有强制编译器构建失败更是一个设计缺陷。(如果您在从函数返回之前运行lambda表达式,这可能很有用,捕获是有效的)。正如其他人所说,您可以获得很多功能,但需要谨慎使用。 - Mat
2
返回指向自动存储的引用/指针,保留具有非const引用的临时变量,保持成员引用对象的引用,其生命周期不绑定到此对象...所有这些都可以在没有lambda的情况下完成。 - Mat
显示剩余7条评论
2个回答

3
这并不是仅限于lambda表达式,当涉及到生命周期时,您可能会做很多糟糕的事情(而且您已经注意到了至少一种情况)。虽然相比于C++03,C++11在多个方面上可能更安全,但C++并没有强调内存安全。
这并不是说C++不想要安全,但我认为通常的哲学“不付出你不使用的代价”通常会妨碍添加安全保护(不考虑像停机问题这样的东西,这可能会阻止发出所有无效程序的诊断)。如果您可以解决升级funarg问题,并且不影响每个其他情况的性能,则标准委员会会感兴趣。(我的意思不是以恶意的方式,我认为这是一个有趣而困难的问题。)
由于您似乎正在进行某些追赶,在lambda表达式中普遍建议不要使用按引用捕获的catch-all捕获(例如[&, foo, bar]),并且要小心引用捕获。您可以将lambda表达式的捕获列表视为C++中必须小心处理生命周期的另一个地方;或者另一种观点是将lambda表达式视为函数对象的对象文字表示法(实际上它们是这样指定的)。在设计与生命周期相关的类类型时,您必须小心:
struct foo {
    explicit foo(T& t)
        : ref(t)
    {}

    T& ref;
};

foo make_foo()
{
    T t;
    // Bad
    return foo { t };
    // Not altogether different from
    // return [&t] {};
}

就写“显而易见”的糟糕代码而言,Lambda表达式并没有改变现状,它们继承了所有现有的注意事项。


1
另一个很大的区别似乎是C++11不会自动捕获环境变量,因此在这个意义上,你必须明确声明你想要捕获一个变量,这样做是没有问题的。我曾经认为所有这些都是自动化的,这似乎更加危险。谢谢! - J D

2

如果您试图保持对内存处理的无知状态,那么在任何复杂度的C++项目中工作都是不可行的。有许多语言比C++更适合实现这种编程范式。C++没有垃圾回收机制的原因是它并不适用于想要使用C++的场景。

话虽如此,在您的lambda示例中,只需要进行一些简单的更改就可以使该示例完全安全:

#include <functional>

std::function<int()> foo(int n) {
  return [=](){return n;}; //now n is copied by value
}

1
如果你试图保持对内存处理的无知,那么在任何复杂程度的C++项目中工作都不是一件简单的事情。我完全同意这点,但Herb声称现代C++是“安全的”,这让我感到困惑。这并不安全... - J D
没有一种编程语言是完全安全的。只要有足够的知识,你就可以通过使用其内部结构或第三方本地库来破解每种语言。 此外,尝试返回局部变量的指针。这和返回引用一样危险,但我猜测它不会引起任何编译器警告... 新的C++在安全方面更加可靠,它允许你比以前更容易地编写安全代码。例如,你可以返回指向std::function动态分配实例的shared_ptr,一切都将顺利进行。 - Spook

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