C++ Lambda捕获右值引用异常行为的原因探究
问题背景
尝试通过Lambda表达式将函数包装为std::function<void()>类型,编写了VoidFunctionWrapper类。该类的wrap_function方法使用[&]引用捕获外部变量,已知此方式存在问题,应改用值捕获[=]。但测试时发现所有包装后的函数均修改同一个变量,输出的指针地址完全相同;原本预期会出现悬垂指针崩溃,但实际并未发生,现疑惑&(check_list[i_check])这个右值在Lambda中的捕获机制,以及为何会导致此现象。
相关代码
VoidFunctionWrapper类代码
class VoidFunctionWrapper { public: VoidFunctionWrapper() { function_ = []() {}; } template <typename Function, typename... Args> VoidFunctionWrapper(Function &&function, Args &&...args) { wrap_function(std::forward<Function>(function), std::forward<Args>(args)...); } void wrap_function(std::function<void()> function) { function_ = std::move(function); } template <typename Function, typename... Args> std::enable_if_t<!std::is_same_v<std::decay_t<Function>, std::function<void()>>, void> wrap_function(Function &&function, Args &&...args) { std::function<void()> function_lambda = [&]() mutable { function(std::forward<Args>(args)...); }; wrap_function(function_lambda); } std::function<void()> get_wrapped_function() { return function_; } private: std::function<void()> function_; };
关键成员函数
template <typename Function, typename... Args> std::enable_if_t<!std::is_same_v<std::decay_t<Function>, std::function<void()>>, void> wrap_function(Function &&function, Args &&...args) { std::function<void()> function_lambda = [&]() mutable { function(std::forward<Args>(args)...); }; wrap_function(function_lambda); }
测试代码
int main() { std::function<void(bool *)> check = [](bool *ptr) { std::cout << ptr << std::endl; *ptr = true; }; std::vector<VoidFunctionWrapper> tasks_to_execute; std::deque<bool> check_list(5, false); for (size_t i_check = 0; i_check < check_list.size(); i_check++) { VoidFunctionWrapper temp_func = VoidFunctionWrapper(check, &(check_list[i_check])); tasks_to_execute.push_back(std::move(temp_func)); } for (size_t i_check = 0; i_check < check_list.size(); i_check++) { tasks_to_execute[i_check].get_wrapped_function()(); } for (size_t i_check = 0; i_check < check_list.size(); i_check++) { std::cout << check_list[i_check] << " "; } std::cout << std::endl; }
测试结果
0x7f9506008804 0x7f9506008804 0x7f9506008804 0x7f9506008804 0x7f9506008804 0 0 0 0 1
核心疑问
Lambda中捕获的右值&(check_list[i_check])是如何工作的,为何会让所有VoidFunctionWrapper实例修改同一个变量?
原因分析
引用捕获的对象是函数形参
[&]会引用捕获Lambda所在作用域的所有外部变量,但这里的args是wrap_function的函数形参——&(check_list[i_check])这个临时指针会被绑定到bool*&&类型的形参上,Lambda捕获的是这个形参的引用,而非check_list[i_check]的地址本身。函数形参的生命周期与栈空间复用
wrap_function是模板函数,每次循环调用时,函数的栈帧结构一致,形参args在栈上的位置固定。当wrap_function执行完毕,形参被销毁,Lambda持有的是悬垂引用。但由于栈空间被复用,每次循环的形参都会占用同一个栈位置,最后一次循环时该位置存储的是&check_list[4]的地址,因此所有Lambda调用时都会读取到这个地址,导致输出相同,且仅最后一个元素被修改为true。未定义行为的偶然性
未出现崩溃是未定义行为的偶然结果——此时栈空间未被其他操作覆盖,刚好保留了最后一次的值。这种情况完全不可靠,换环境或修改代码极可能引发崩溃或异常。
解决方案
要避免悬垂引用并正确捕获参数值,需修改Lambda的捕获方式:
- 改用值捕获
[=],将function和args的值拷贝到Lambda中; - 或显式捕获并完美转发参数,避免捕获不必要的引用:
template <typename Function, typename... Args> std::enable_if_t<!std::is_same_v<std::decay_t<Function>, std::function<void()>>, void> wrap_function(Function &&function, Args &&...args) { std::function<void()> function_lambda = [func = std::forward<Function>(function), args_tuple = std::make_tuple(std::forward<Args>(args)...)]() mutable { std::apply(std::move(func), std::move(args_tuple)); }; wrap_function(function_lambda); }
内容的提问来源于stack exchange,提问作者Christ Liu

