Асинхронные функции 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 использует оператор(), сигнатура которого соответствует серии асинхронных операций с подписью CompletionToken.
Однако у async_write() и async_resolve разные подписи CompletionToken.
Как лучше всего написать код сопрограммы без стека с другой подписью CompletionToken?
С++ 17 Повышение 1.82.0
Я написал код разрешения-подключения-записи несколькими способами. Пока что 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();
}
Актерский состав не требуется. Шаблон создавался несколько раз, но случай переключения на основе coro работает хорошо.
#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();
}
Одним из больших преимуществ бесстековых сопрограмм является непрерывный код. Этот подход теряет преимущество, поэтому я его не выбираю.





ИМХО, правильный ответ - иметь несколько перегрузок оператора (), которые вы сразу же отклоняете. (обоснование ниже¹)
Конечно, у вас может быть лучшее из обоих миров:
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)
{
Смотрите Прямой эфир на Колиру²
#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();
}
Выход
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
¹ Основная причина заключается в том, что он лучше работает со «стабильными» сопрограммами, где сам сопрограмма передается в качестве аргумента «я», что позволяет избежать копирования всего состояния сопрограммы, вместо этого просто перемещает владение одним (динамическим) распределением.
Другая причина заключается в том, что это позволяет мне кодировать конечный автомат с разрешением перегрузки, избегая опасно незаметных сюрпризов вокруг макросов reenter и yield.
² У COLIRU нет службы asio::consign или DNS.
Ах, оператор() перегружает вызов call(). Я не думал об этом. Я думал, что каждая перегрузка имеет другой объект boost::asio::coroutine как coro1, coro2... поэтому я не выбирал его. Ваш подход кажется хорошим. Это очень простой и эффективный код. Спасибо!
На самом деле нет никакой разницы, если вы назовете (ха) call() другое имя, например operator(): coliru.stacked-crooked.com/a/0e691d19194cdba8 Я просто думаю, что call, invoke или даже entrypoint более понятное имя. Также это позволяет избежать путаницы с перегрузками.
Просто для сравнения, вот мой подход, основанный на перегрузке: coliru.stacked-crooked.com/a/c7a7c07b746f88c0 . Вы можете заставить его работать с неполными типами, поэтому вы можете объявлять состояния, используя states = std::tuple<struct resolveS, struct connectS, struct writeS>; , или вы можете указать, что состояния могут содержать элементы: coliru.stacked-crooked.com/a/72d6794f4cd41596
Нашел дополнительное время, чтобы вспомнить, как делать stable_op (поэтому вам не нужно make_shared данные участника, например). coliru.stacked-crooked.com/a/081afb2bf62af788
Поскольку вы уже знаете все возможные типы во время компиляции, вы также можете использовать
std::variantвместоstd::any, что позволяет избежать дополнительных выделений кучи для случаяstd::size_t. (Если вы передаете второй аргумент по значению или требуется преобразование в тип-оболочку, у вас всегда будут выделения для сложных типов, таких как результаты распознавателя, поскольку они будут скопированы. Вы можете обойти это с помощьюreference_wrapperдля тех типов, хотя я этого не пробовал.) Кроме этого, я не могу придумать никакого другого прямого способа, о котором вы еще не подумали.