C++11:预防lambda作用域捕获错误

4

C++中,与C#等语言不同的是,可以在lambda表达式中指定是否以值或引用方式捕获封闭作用域变量。这导致了未定义情况,即可能将一个封闭作用域通过引用捕获的lambda传递给在调用lambda表达式之前返回的函数:

void test()
{
    int t = 1;
    enqueue_task([&]() { do_something(t); });
}

在这种情况下,当Lambda表达式所指定的任务被调度执行时,“t”很可能会超出作用域。显然,这会导致难以解决的错误。

我的解决方案是添加如下语言特性:

template<class T>
void enqueue_task(T lambda)
{
    static_assert(!std::is_lambda<T>::value || std::is_lambda_captured_by_value<T>::value,
        "The lambda expression is executed asynchronously and therefore capturing eclosing state via reference is forbidden.");

    // enqueue task for execution
}

对我而言,这将是一种干净的“非侵入式”扩展,可让中间件编写者保护其API免受误用。当然,它并不能提供防弹保护,因为我仍然可以通过值传递指向堆栈对象的指针,可能还有更多方式。不管怎样,即使通过值传递时仍会默默地导致未定义行为的代码本身可能就是有问题的。
有没有类似的已经支持的解决方案?
对我而言,目前一个明智的解决方案似乎是在延迟执行情况下根本不允许任何lambda表达式。例如,事件处理程序不应该是lambda类型。这是说得容易做起来难,因为这也意味着我不能使用std :: function,而必须回到老式函数类型。
甚至更好的方法是引入一种关键字,比如:
void test()
{
    int t = 1;
    enqueue_task(deferred () { do_something(t); });
}

这将确保编译器可以,以任何方式,使传递的lambda函数适合延迟执行,这意味着当其封闭范围消失时。

我认为C++11已经走了很长一段路来提高C++编程的安全性。这个lambda东西是你仍然在脚下指着一把枪的少数几个地方之一。它只是一个定时炸弹。


目前一个明智的解决方案似乎是在延迟执行的情况下根本不允许任何lambda表达式。这样一来,你就失去了形成闭包的能力。 - JAB
@JAB:当然,我不是指标准上不允许使用,而是从API的角度考虑。如果Lambda表达式对你的API的可用性没有贡献,并且存在用户可能会忘记你的API调用了延迟Lambda表达式的潜在风险,那么你就不应该在这个API中使用Lambda表达式。一个API应该强制正确的使用方式。 - thesaint
足够真实,但像Lambda表达式一样,它并不是你可以获得你担心的问题的唯一方法。如果用户传递一个涉及超出范围引用的非Lambda函数会发生什么?或者更糟糕的是,原始指针?唯一真正强制正确使用API的方法是防止用户提供任何输入(即使没有这样做,如果您不小心设置约束条件,则可能会得到虚假阳性,在这些情况下,有效参数被拒绝,因为它们的设置方式不完全符合您的要求)。 - JAB
请注意,C++14 显然支持在 lambda 中通过移动进行捕获,这可能是解决您的问题的(未来)解决方案。http://scottmeyers.blogspot.com/2013/05/c14-lambdas-and-perfect-forwarding.html - JAB
我还没有完全看出如何将其用于我的问题:P。非lambda并不是那么危险,因为人们会更加小心(我希望如此)。但我自己也曾多次写过后来被引用的lambda -.-。在那里犯错误太容易了。 - thesaint
显示剩余2条评论
1个回答

6
通常的解决方法是通过值进行捕获 [=]() {...}
当复制实际对象不可行时,通常最好使用 shared_ptr 来使用它,这可能会提供更便宜的复制,具体情况取决于上下文,并且还允许您共享所有权,使调用方和延迟的lambda可以独立地使用它。
C++14应该具有移动捕获语义,这将解决在不需要共享时复制对象的性能问题。
否则,您需要传递引用。就像在C++中的所有内容(不仅仅是在lambda中),当您开始传递指针和引用时,您需要小心。

好的,我将这个标记为答案,尽管JAB之前已经提到了这件事,但只是作为评论:P。 - thesaint
@thesaint 注意,他说的是其中的一部分。在我看来,这里重要的部分是按值捕获;当您需要同一个对象时,使用shared_ptr按值引用该对象。由于原子操作的原因,在某些场景下,复制shared_ptr仍然可能会有点昂贵,这就是为什么我提到了即将推出的移动语义。 - Yam Marcovic
@thesaint 这并没有真正回答问题。问题是我们是否可以像 static_assert is_lambda_captured_by_value 一样做些什么?答案是,大概不能。我找到的最接近的是这个:https://stackoverflow.com/a/39890486/7098259 - Patrick Parker

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