如何编写适配不同CompletionToken签名的Boost.Asio无栈协程代码?
适配不同CompletionToken签名的Boost.Asio无栈协程最佳实现方式
问题背景
Boost.Asio的异步函数对应不同的CompletionToken签名,例如:
boost::asio::async_write对应的WriteToken处理函数签名:
void write_handler( const boost::system::error_code& ec, std::size_t bytes_transferred)
boost::asio::ip::tcp::resolver::async_resolve对应的ResolveToken处理函数签名:
void resolve_handler( const boost::system::error_code& ec, boost::asio::ip::tcp::resolver::results_type results);
二者首个参数均为const boost::system::error_code&,但第二个参数类型不同。而Boost.Asio的无栈协程要求operator()的签名匹配所有异步操作的CompletionToken签名,这就导致async_write和async_resolve的签名无法直接兼容。
环境
- C++17
- Boost 1.82.0
已尝试的实现方式
1. 使用std::any
通过std::any统一第二个参数类型,需要用std::any_cast做类型转换,类型不匹配时会抛出异常,整体效果尚可:
#include <iostream> #include <any> #include <boost/asio.hpp> #include <boost/asio/yield.hpp> struct myapp { myapp( boost::asio::ip::tcp::resolver& res, boost::asio::ip::tcp::socket& sock ): res{res}, sock{sock} {} void operator()( boost::system::error_code const& ec = boost::system::error_code{}, std::any second = std::any{} ) { reenter (coro) { // resolve yield res.async_resolve("localhost", "1883", *this); std::cout << "async_resolve:" << ec.message() << std::endl; if (ec) return; // connect yield { auto results = std::any_cast<boost::asio::ip::tcp::resolver::results_type>(second); boost::asio::async_connect( sock, results.begin(), results.end(), *this ); } std::cout << "async_connect:" << ec.message() << std::endl; if (ec) return; // write yield { auto buf = std::make_shared<std::string>("hello"); boost::asio::async_write( sock, boost::asio::buffer(*buf), boost::asio::consign( *this, buf ) ); } std::cout << "async_write:" << ec.message() << " bytes transferred:" << std::any_cast<std::size_t>(second) << std::endl; } } boost::asio::ip::tcp::resolver& res; boost::asio::ip::tcp::socket& sock; boost::asio::coroutine coro; }; #include <boost/asio/unyield.hpp> int main() { boost::asio::io_context ioc; boost::asio::ip::tcp::resolver res{ioc.get_executor()}; boost::asio::ip::tcp::socket sock{ioc.get_executor()}; myapp ma{res, sock}; ma(); // start coroutine ioc.run(); }
2. 使用带if constexpr的模板operator()
通过模板参数接收第二个参数,配合if constexpr做类型判断,无需类型转换,但会实例化多个模板版本,协程的switch逻辑可以正常运行:
#include <iostream> #include <type_traits> #include <boost/asio.hpp> #include <boost/asio/yield.hpp> struct myapp { myapp( boost::asio::ip::tcp::resolver& res, boost::asio::ip::tcp::socket& sock ): res{res}, sock{sock} {} template <typename Second = std::nullptr_t> void operator()( boost::system::error_code const& ec = boost::system::error_code{}, Second&& second = nullptr ) { reenter (coro) { // resolve yield res.async_resolve("localhost", "1883", *this); std::cout << "async_resolve:" << ec.message() << std::endl; if (ec) return; // connect yield { if constexpr( std::is_same_v<std::decay_t<Second>, boost::asio::ip::tcp::resolver::results_type> ) { boost::asio::async_connect( sock, second.begin(), second.end(), *this ); } } std::cout << "async_connect:" << ec.message() << std::endl; if (ec) return; // write yield { auto buf = std::make_shared<std::string>("hello"); boost::asio::async_write( sock, boost::asio::buffer(*buf), boost::asio::consign( *this, buf ) ); } if constexpr( std::is_same_v<std::decay_t<Second>, std::size_t> ) { std::cout << "async_write:" << ec.message() << " bytes transferred:" << second << std::endl; } } } boost::asio::ip::tcp::resolver& res; boost::asio::ip::tcp::socket& sock; boost::asio::coroutine coro; }; #include <boost/asio/unyield.hpp> int main() { boost::asio::io_context ioc; boost::asio::ip::tcp::resolver res{ioc.get_executor()}; boost::asio::ip::tcp::socket sock{ioc.get_executor()}; myapp ma{res, sock}; ma(); // start coroutine ioc.run(); }
3. 编写多个重载operator()
这种方式会破坏无栈协程代码连贯性的核心优势,因此不做考虑。
最佳实现方式分析
对比两种可行方案,带if constexpr的模板operator()是更优的选择,理由如下:
- 类型安全:无需
std::any_cast的运行时类型转换,完全在编译期完成类型匹配,避免了类型不匹配导致的运行时异常。 - 性能更优:编译期处理类型逻辑,没有
std::any带来的额外内存开销和运行时转换成本。 - 代码可读性:通过
if constexpr明确区分不同类型分支,逻辑清晰,后续维护更方便。
另外,还可以进一步优化模板版本的代码,比如将类型判断的逻辑封装成辅助 trait,但基础的if constexpr模板方案已经能很好地解决问题。需要注意的是,模板operator()会根据不同的第二个参数类型实例化多个版本,但这是编译期的开销,不会影响运行时性能,且对于固定的异步操作流程来说,实例化的版本数量是可控的。
内容的提问来源于stack exchange,提问作者Takatoshi Kondo
相关产品推荐
相关产品推荐

