Lambda表达式移动捕获时机探究:Boost.Asio中shared_ptr捕获安全性疑问
关于C++移动捕获lambda与Boost.Asio调用的求值顺序问题
这是一个非常典型的C++标准求值顺序差异导致的行为不一致问题,我们可以从标准规则和编译器实现两个层面来拆解:
核心问题:函数调用的求值顺序规则
你的代码中关键的风险点在于这句:
tim->async_wait([&, tim = std::move(tim)](const boost::system::error_code& ec) { ... });
这里涉及两个关键操作的求值顺序:
- 成员函数调用的前置部分:
tim->async_wait(需要先通过operator->获取deadline_timer的指针,进而拿到async_wait函数地址) - lambda表达式的初始化捕获:
tim = std::move(tim)(将原shared_ptr的所有权转移到lambda的捕获变量中,原tim会变为空)
C++17之前:未指定行为(存在未定义行为风险)
在C17标准发布前,C对于函数调用表达式E1(E2)的求值顺序没有强制规定——编译器可以自由选择先求值E1(即tim->async_wait)还是先求值E2(即包含移动捕获的lambda)。
这就直接导致了不同编译器的表现差异:
- g++6.3.0的行为:编译器选择先执行lambda的移动捕获,原
tim变为空指针,之后再求值tim->async_wait时,调用operator->会解引用空指针,触发段错误。 - 高版本g++/clang的行为:编译器选择先求值
tim->async_wait,获取到deadline_timer的有效指针并准备调用async_wait,之后再执行移动捕获。此时async_wait已经完成了对定时器对象的引用,lambda持有转移后的shared_ptr保证对象存活,因此程序正常执行。
由于存在一种编译器路径会导致空指针解引用(未定义行为),所以在C++17之前,这段代码的整体行为是未定义的。
C++17及以后:明确的安全顺序
C++17标准修正了这个模糊点,明确规定:函数调用中的函数表达式(E1)必须在所有参数表达式(E2)之前完成求值。
对应到你的代码,就是必须先完成tim->async_wait的求值(包括通过operator->获取有效定时器指针),再处理lambda的移动捕获。此时原tim的移动不会影响async_wait的调用,lambda持有的shared_ptr会保证deadline_timer对象存活到回调执行完毕,代码是完全安全的。
兼容旧标准的安全写法
如果需要兼容C++17之前的编译器,建议避免这种可能触发求值顺序问题的写法,改用更稳妥的方式:
复制捕获shared_ptr:
shared_ptr的复制开销极小(只是原子操作增加引用计数),完全可以接受:tim->async_wait( [&, tim = tim](const boost::system::error_code& ec) { std::cout << ec.message() << std::endl; } );提前准备捕获变量:
先复制出一个临时shared_ptr,再移动捕获这个临时变量,确保原tim始终有效:auto temp_tim = tim; tim->async_wait( [&, tim = std::move(temp_tim)](const boost::system::error_code& ec) { std::cout << ec.message() << std::endl; } );
内容的提问来源于stack exchange,提问作者Takatoshi Kondo
相关产品推荐
相关产品推荐

