Question
Boost.Asio's asynchronous functions have various CompletionToken signature. For example, boost::asio::async_write
has WriteToken.
void write_handler(
const boost::system::error_code& ec,
std::size_t bytes_transferred)
boost::asio::ip::tcp::resolver::async_resolve
has ResolveToken.
void resolve_handler(
const boost::system::error_code& ec,
boost::asio::ip::tcp::resolver::results_type results);
The first argument types are both const boost::system::error_code&
but the second argument types are different.
Stackless Coroutine uses operator() that's sigunature matchs the series of async operations CompletionToken signature.
However, async_write()
and async_resolve
have different CompletionToken signature.
What is the best way to write the stackless coroutine code with different CompletionToken signature ?
Environment
C++17 Boost 1.82.0
What I tried
I wrote resolve-connect-write code using several way. So far, std::any
seems good but perhaps there would be better way.
Use std::any
It requires std::any_cast
. If the type is mismatched, then exception is thrown. not bad.
#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();
}
Use template with if constexpr
No cast required. Template instantiated multiple times, but coro
based switch case seems to work well.
#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:"
<< 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();
}
Prepare multiple operator()()
One of the big advantage of stackless coroutine is continuous code. This approach lose the advantage so I don't choose it.