C++20协程中CompletionHandler未调用也未存储的行为探究
在使用Boost.Asio的CompletionToken机制时,我原本设计由CompletionToken生成的completion_handler最终会被调用。但我好奇如果completion_handler既不被调用也不被存储时的行为——此时completion_handler会被直接销毁。
我使用C++20协程结合use_awaitable CompletionToken进行了测试,观察到特殊行为:执行co_await后进入completion_handler销毁路径,栈展开,main()中的ioc.run()结束,且过程中未捕获到任何异常。请问这是预期行为还是未定义行为?如果是预期行为,相关文档在哪里?目前我尚未找到。
测试代码
#include <iostream> #include <boost/asio.hpp> namespace as = boost::asio; template <typename CompletionToken> auto func(bool call, CompletionToken&& token) { auto init = [] ( auto completion_handler, bool call ) { if (call) { std::move(completion_handler)(); } else { // What is happend if completion_handler is neither invoked nor stored ? } }; return as::async_initiate< CompletionToken, void() >( init, token, call ); } struct trace { trace() { std::cout << __PRETTY_FUNCTION__ << std::endl; } ~trace() { std::cout << __PRETTY_FUNCTION__ << std::endl; } }; as::awaitable<void> proc1() { trace t; // destructed correctly try { { std::cout << "before call=true" << std::endl; co_await func(true, as::use_awaitable); std::cout << "after call=true" << std::endl; } { std::cout << "before call=false" << std::endl; co_await func(false, as::use_awaitable); // the following part is never executed std::cout << "after call=false" << std::endl; } } catch (...) { std::cout << "caught exception" << std::endl; } std::cout << "co_return" << std::endl; co_return; } as::awaitable<void> proc2() { for (int i = 0; i != 2; ++i) { std::cout << "before proc1" << std::endl; co_await proc1(); std::cout << "after proc1" << std::endl; } } int main() { as::io_context ioc; as::co_spawn(ioc.get_executor(), proc2, as::detached); ioc.run(); std::cout << "finish" << std::endl; }
输出结果
before proc1 trace::trace() before call=true after call=true before call=false trace::~trace() finish
结论与说明
这种行为属于未定义行为。Boost.Asio的设计契约要求:所有基于CompletionToken的异步操作,必须最终调用生成的completion_handler。对于use_awaitable这类绑定协程的CompletionToken来说,未调用completion_handler就销毁它,会直接终止对应的协程(无异常抛出),导致栈展开;由于没有剩余待处理的异步任务,io_context.run()会直接退出。
Boost.Asio的官方文档中,虽然没有直接针对该场景的明确描述,但在CompletionToken的核心模型说明中隐含了这一要求:异步操作的完成回调是整个异步流程的必要环节,跳过调用会破坏异步操作的状态一致性,引发未定义行为。
实际使用的正确方案
实际开发中,应避免这种未调用回调的情况,建议采用带错误码的回调设计,明确告知调用方操作结果:
template <typename CompletionToken> auto func(bool call, CompletionToken&& token) { auto init = [] ( auto completion_handler, bool call ) { if (call) { std::move(completion_handler)(boost::system::error_code{}); } else { // invoke with error code std::move(completion_handler)( boost::system::errc::make_error_code( boost::system::errc::operation_canceled ) ); } }; return as::async_initiate< CompletionToken, void(boost::system::error_code const&) >( init, token, call ); }
内容的提问来源于stack exchange,提问作者Takatoshi Kondo

