Бесстековая сопрограмма Boost.Asio с различными подписями CompletionToken

Вопрос

Асинхронные функции 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();
}

Использовать шаблон с if constexpr

Актерский состав не требуется. Шаблон создавался несколько раз, но случай переключения на основе 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();
}

Подготовить несколько операторов()()

Одним из больших преимуществ бесстековых сопрограмм является непрерывный код. Этот подход теряет преимущество, поэтому я его не выбираю.

Поскольку вы уже знаете все возможные типы во время компиляции, вы также можете использовать std::variant вместо std::any, что позволяет избежать дополнительных выделений кучи для случая std::size_t. (Если вы передаете второй аргумент по значению или требуется преобразование в тип-оболочку, у вас всегда будут выделения для сложных типов, таких как результаты распознавателя, поскольку они будут скопированы. Вы можете обойти это с помощью reference_wrapper для тех типов, хотя я этого не пробовал.) Кроме этого, я не могу придумать никакого другого прямого способа, о котором вы еще не подумали.

chris_se 17.04.2023 09:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
1
69
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

ИМХО, правильный ответ - иметь несколько перегрузок оператора (), которые вы сразу же отклоняете. (обоснование ниже¹)

Конечно, у вас может быть лучшее из обоих миров:

    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... поэтому я не выбирал его. Ваш подход кажется хорошим. Это очень простой и эффективный код. Спасибо!

Takatoshi Kondo 17.04.2023 15:25

На самом деле нет никакой разницы, если вы назовете (ха) call() другое имя, например operator(): coliru.stacked-crooked.com/a/0e691d19194cdba8 Я просто думаю, что call, invoke или даже entrypoint более понятное имя. Также это позволяет избежать путаницы с перегрузками.

sehe 17.04.2023 16:49

Нашел дополнительное время, чтобы вспомнить, как делать stable_op (поэтому вам не нужно make_shared данные участника, например). coliru.stacked-crooked.com/a/081afb2bf62af788

sehe 18.04.2023 02:55

Другие вопросы по теме