将Boost异步函数封装为协程时返回值异常问题求助
尝试将基于Boost、使用完成处理器(completion handlers)的第三方库类封装为Boost协程使用。简化代码中,期望async_connect返回的awaitable类型为boost::system::error_code,但实际返回void。后续修改完成处理器签名,加入std::exception_ptr作为第一个参数后,结果能正确从协程返回,但不理解该签名要求的原因。
原始简化代码:
#include <boost/asio.hpp> #include <boost/asio/awaitable.hpp> #include <boost/asio/co_spawn.hpp> #include <iostream> using AsyncHandler = std::function<void(boost::system::error_code)>; struct LibraryClient { LibraryClient(boost::asio::io_context& ioc) : socket{ioc} {} boost::asio::ip::tcp::socket socket; void async_connect(AsyncHandler handler = {}) { boost::system::error_code ec; boost::asio::ip::address ip_address = boost::asio::ip::address::from_string("127.0.0.1", ec); boost::asio::ip::tcp::endpoint ep(ip_address, 9999); socket.async_connect(ep, std::move(handler)); } }; template<class CompletitionToken = boost::asio::use_awaitable_t<>> auto do_async_connect(LibraryClient& client, CompletitionToken&& token = {}) { auto initiate = [&client]<class H>(H&& self) mutable { client.async_connect([self = std::make_shared<H>(std::forward<H>(self))](auto&& r) { (*self)(r); }); }; return boost::asio::async_initiate< CompletitionToken, boost::system::error_code(boost::system::error_code) >(initiate, token); } struct LibraryClientWrapper { LibraryClient client; boost::asio::awaitable<boost::system::error_code> async_connect() { const auto ec = co_await do_async_connect(client); } }; int main() { auto ioc = boost::asio::io_context{}; auto client = LibraryClientWrapper{LibraryClient{ioc}}; ioc.run(); }
编辑后的测试代码(验证签名修改有效):
#include <boost/asio.hpp> #include <boost/asio/awaitable.hpp> #include <boost/asio/co_spawn.hpp> #include <iostream> #include <cassert> template<class CompletitionToken> auto do_async_connect(LibraryClient& client, CompletitionToken&& token) { auto initiate = [](auto&& handler) { handler(nullptr, 90); }; return boost::asio::async_initiate<CompletitionToken, void(std::exception_ptr, int)>( initiate, std::forward<CompletitionToken>(token) ); } struct LibraryClientWrapper { LibraryClient client; boost::asio::awaitable<void> async_connect() { const auto ec = co_await do_async_connect(client, boost::asio::use_awaitable); assert(ec == 90); } }; void rethrow_exception(std::exception_ptr eptr) { if (eptr) { std::rethrow_exception(eptr); } } int main() { auto ioc = boost::asio::io_context{}; auto client = LibraryClientWrapper{LibraryClient{ioc}}; boost::asio::co_spawn(ioc, client.async_connect(), rethrow_exception); ioc.run(); }
核心原因
Boost Asio的协程机制(基于use_awaitable令牌)要求完成处理器的签名必须遵循特定约定:第一个参数为std::exception_ptr,后续参数为操作的结果值。
这个约定的设计目的是统一处理两种错误传递方式:
- 如果异步操作通过抛出异常报告错误,Asio会将异常封装为
std::exception_ptr传入处理器; - 如果操作通过返回错误码(如
boost::system::error_code)报告错误,处理器的第一个参数传入nullptr,后续参数传递结果值。
你的原始代码中,async_initiate指定的签名是boost::system::error_code(boost::system::error_code),不符合Asio协程的签名要求,导致Asio无法正确将error_code映射为co_await的返回值,最终awaitable类型退化为void。
解决方案
修改do_async_connect函数中async_initiate的签名,以及调用完成处理器的方式,符合Asio的约定:
template<class CompletitionToken = boost::asio::use_awaitable_t<>> auto do_async_connect(LibraryClient& client, CompletitionToken&& token = {}) { auto initiate = [&client]<class H>(H&& self) mutable { client.async_connect([self = std::make_shared<H>(std::forward<H>(self))](boost::system::error_code ec) { // 第一个参数传nullptr(无异常),第二个参数传实际的error_code (*self)(nullptr, ec); }); }; // 签名改为void(std::exception_ptr, boost::system::error_code) return boost::asio::async_initiate< CompletitionToken, void(std::exception_ptr, boost::system::error_code) >(std::move(initiate), std::forward<CompletitionToken>(token)); }
修改后,LibraryClientWrapper::async_connect中的co_await就能正确获取到boost::system::error_code作为返回值,awaitable类型也会符合预期。
补充说明
编辑后的测试代码中,加入std::exception_ptr作为第一个参数后,int值能正确返回,本质就是符合了Asio协程的签名约定:当第一个参数为nullptr时,co_await会将后续参数作为返回值。
内容的提问来源于stack exchange,提问作者bielu000

