英文:
Boost.Asio stackless coroutine with different CompletionToken signatures
问题
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&
,但第二个参数类型不同。
Stackless Coroutine使用的operator()
的签名与一系列异步操作的CompletionToken签名匹配。
然而,async_write()
和async_resolve
具有不同的CompletionToken签名。
如何以最佳方式编写具有不同CompletionToken签名的stackless coroutine代码?
环境:C++17,Boost 1.82.0
我尝试了什么:
- 使用
std::any
// 代码示例已略过
- 使用带有if constexpr的模板
// 代码示例已略过
- 准备多个
operator()()
以上是您提供的内容的翻译。如果您需要更多信息或有其他问题,请随时提出。
英文:
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.
答案1
得分: 2
IMHO,正确的答案是要有多个operator()的重载版本,这一点您已经一概拒绝了。 (下文提供了理由)。
当然,您也可以兼顾两者:
void operator()() { return call(); }
void operator()(error_code ec, size_t n) { return call(ec, n); }
void operator()(error_code ec, endpoints eps) { return call(ec, 0, std::move(eps)); }
void operator()(error_code ec, endpoint /*unused*/) { return call(ec, 0); }
私有部分:
private:
void call(
error_code ec = {},
size_t bytes_transferred = {},
std::optional<endpoints> eps = {})
{
reenter(coro)
{
详细内容请参考链接:在Coliru上查看。
英文:
IMHO the correct answer is to have multiple overloads of operator(), which you dismiss out of hand. (rationale below¹)
You might of course have the best of both worlds:
void operator()() { return call(); }
void operator()(error_code ec, size_t n) { return call(ec, n); }
void operator()(error_code ec, endpoints eps) { return call(ec, 0, std::move(eps)); }
void operator()(error_code ec, endpoint /*unused*/) { return call(ec, 0); }
private:
void call(
error_code ec = {},
size_t bytes_transferred = {},
std::optional<endpoints> eps = {})
{
reenter(coro)
{
See it Live On Coliru²
#include <iostream>
#include <optional>
#include <boost/asio.hpp>
#include <boost/asio/yield.hpp>
namespace asio = boost::asio;
using asio::ip::tcp;
struct myapp {
using error_code = boost::system::error_code;
using endpoint = tcp::endpoint;
using endpoints = tcp::resolver::results_type;
myapp(
tcp::resolver& res,
tcp::socket& sock
): res{res}, sock{sock}
{
}
void operator()() { return call(); }
void operator()(error_code ec, size_t n) { return call(ec, n); }
void operator()(error_code ec, endpoints eps) { return call(ec, 0, std::move(eps)); }
void operator()(error_code ec, endpoint /*unused*/) { return call(ec, 0); }
private:
void call(
error_code ec = {},
size_t bytes_transferred = {},
std::optional<endpoints> eps = {})
{
reenter(coro)
{
// resolve
yield res.async_resolve("localhost", "1883", *this);
std::cout << "async_resolve:" << ec.message() << std::endl;
if(ec) return;
// connect
yield asio::async_connect(sock, *eps, *this);
std::cout << "async_connect:" << ec.message() << std::endl;
if (ec) return;
// write
yield {
auto buf = std::make_shared<std::string>("hello\n");
asio::async_write(
sock,
asio::buffer(*buf),
asio::consign(
*this,
buf
)
);
}
std::cout << "async_write:" << ec.message()
<< " bytes transferred:" << bytes_transferred
<< std::endl;
}
}
tcp::resolver& res;
tcp::socket& sock;
asio::coroutine coro;
};
#include <boost/asio/unyield.hpp>
int main()
{
asio::io_context ioc;
tcp::socket sock{ioc};
tcp::resolver res{sock.get_executor()};
myapp ma{res, sock};
ma(); // start coroutine
ioc.run();
}
Output
g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp
nc -lp 1883& sleep 1; ./a.out; kill %1
async_resolve:Success
async_connect:Success
async_write:Success bytes transferred:6
hello
¹ The main reason is that it plays better with "stable" coroutines, where the coro itself is passed as the "self" argument which avoids copying all of the coro state, instead just moving ownership of a single (dynamic) allocation.
Another reason is that it allows me to code the state machine with overload resolution, avoiding dangerously subtle surprises around the reenter
and yield
macros.
² COLIRU doesn't have asio::consign
or DNS service
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论