c++20boost-asioclang++c++-coroutineboost-beast

Why can't the result of boost::beast::http::async_write with token as_tuple(deferred) be converted to awaitable<tuple<error_code, size_t>>?


Clang tells me there is no viable conversion from the return value of boost::beast::http::async_write with completion token boost::asio::as_tuple(boost::asio::deferred) to awaitable<tuple<boost::system::error_code, unsigned long>>:

error: no viable conversion from returned value of type 'decltype(enable_if_t<enable_if_t<detail::are_completion_signatures<void (error_code, unsigned long)>::value, detail::async_result_has_initiate_memfn<as_tuple_t<deferred_t>, void (error_code, unsigned long)>>::value, async_result<decay_t<as_tuple_t<deferred_t>>, void (error_code, unsigned long)>>::initiate(static_cast<boost::beast::http::detail::run_write_msg_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp>> &&>(initiation), static_cast<boost::asio::as_tuple_t<boost::asio::deferred_t> &&>(token), static_cast<const boost::beast::http::message<false, boost::beast::http::basic_string_body<char>> *&&>(args), static_cast<std::integral_constant<bool, true> &&>(args)))' (aka 'deferred_async_operation<void (std::tuple<boost::system::error_code, unsigned long>), boost::asio::async_result<boost::asio::as_tuple_t<boost::asio::deferred_t>, void (boost::system::error_code, unsigned long)>::init_wrapper<boost::beast::http::detail::run_write_msg_op<boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::any_io_executor>>>, const boost::beast::http::message<false, boost::beast::http::basic_string_body<char, std::char_traits<char>, std::allocator<char>>, boost::beast::http::basic_fields<std::allocator<char>>> *, std::integral_constant<bool, true>>') to function return type 'boost::asio::awaitable<std::tuple<boost::system::error_code, std::size_t>>' (aka 'awaitable<tuple<boost::system::error_code, unsigned long>>')
  179 |return boost::beast::http::async_write(stream, typed_message, boost::asio::as_tuple(boost::asio::deferred));
      |       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The function in question looks like this:

template<typename stream_t>
boost::asio::awaitable<void> response_worker(boost::asio::experimental::channel<void(boost::system::error_code, http_response_t)> &channel, stream_t &stream)
{
    while (true) {
        boost::system::error_code ec;
        std::variant<
            boost::beast::http::response<boost::beast::http::string_body>,
            boost::beast::http::response<boost::beast::http::empty_body>,
            boost::beast::http::response<boost::beast::http::dynamic_body>,
            boost::beast::http::response<boost::beast::http::file_body>,
            boost::beast::http::response<boost::beast::http::buffer_body>>
            message = co_await channel.async_receive(boost::asio::redirect_error(boost::asio::deferred, ec));
        if (ec == boost::asio::error::eof) {
            channel.close();
            co_return;
        }
        const auto [ec2, transferred] = co_await std::visit(
            [&stream](auto &typed_message) -> boost::asio::awaitable<std::tuple<boost::system::error_code, std::size_t>> {
                return boost::beast::http::async_write(stream, typed_message, boost::asio::as_tuple(boost::asio::deferred));
            },
            message);
        //error handling
    }
}

This used to compile and I'm incredibly confused by the error message, is there an error in my code or is my clang installation even more broken than I thought (libclang does not get the header search correct means I have to add -I/usr/lib/clang/19/include as a compiler flag if I understand it right)?


Solution

  • Reading the compiler message:

    error: no viable conversion from returned value of type 'decltype(...)` (aka 
        deferred_async_operation<
            void(std::tuple<boost::system::error_code, unsigned long>),
            asio::async_result<asio::as_tuple_t<asio::deferred_t>,
                                      void(boost::system::error_code, unsigned long)>::
                init_wrapper<http::detail::run_write_msg_op<
                    asio::basic_stream_socket<asio::ip::tcp, asio::any_io_executor>>>,
            http::message<
                false, http::basic_string_body<char, std::char_traits<char>, std::allocator<char>>,
                http::basic_fields<std::allocator<char>>> const*,
            std::integral_constant<bool, true>>'
              )
                  to function return type
          'asio::awaitable<std::tuple<boost::system::error_code, std::size_t>>'
          return http::async_write(stream, typed_message,
                                                 asio::as_tuple(asio::deferred));
    

    It looks like you may have changed from asio::use_awaitable (which does return an awaitable promise type), to asio::deferred (which results in a deferred async operation).

    This is good, because

    1. deferred became the default completion token
    2. deferred incurs less overhead when called ("transformed") from inside an Asio coroutine frame (awaitable promise type)

    This means you will have to await the deferred operation to get a compatible return type. Note also that recent Asio versions have included partial applications of the token adaptors so you can spell it like so:

    Live On Coliru

    while (true) {
        if (auto [ec, message] = co_await channel.async_receive(asio::as_tuple); ec == asio::error::eof) {
            co_return channel.close();
        } else if (auto [ec, transferred] = co_await std::visit(
                       [&](auto& typed_message) -> asio::awaitable<std::tuple<error_code, std::size_t>> {
                           co_return co_await async_write(stream, typed_message, asio::as_tuple);
                       },
                       message);
                   ec.failed()) //
        {
            // error handling
        } else {
            // success handling
        }
    }
    

    HOLD ON

    However, please note that you don't need to be so ... clumsy about variant response objects. You can type-erase the response using a BuffersGenerator. The implementation that supports all message<> instances is called http::message_generator. E.g.:

    #include <boost/asio.hpp>
    #include <boost/asio/experimental/channel.hpp>
    #include <boost/beast.hpp>
    namespace asio = boost::asio;
    
    namespace http        = boost::beast::http;
    using http_response_t = std::optional<http::message_generator>;
    using channel_t       = asio::experimental::channel<void(boost::system::error_code, http_response_t)>;
    
    template <typename stream_t> asio::awaitable<void> response_worker(channel_t& channel, stream_t& stream) {
        using boost::beast::error_code;
    
        while (true) {
            if (auto [ec, response] = co_await channel.async_receive(asio::as_tuple); ec == asio::error::eof) {
                co_return channel.close();
            } else if (response.has_value()) {
                if (auto [ec, transferred] =
                        co_await boost::beast::async_write(stream, std::move(*response), asio::as_tuple);
                    ec.failed()) //
                {
                    // error handling
                } else {
                    // success handling
                }
            }
        }
    }
    
    int main() {
        asio::io_context      io_context;
        asio::ip::tcp::socket socket(io_context); // TODO connect
        channel_t             channel(io_context);
    
        co_spawn(io_context, response_worker(channel, socket), asio::detached);
    
        io_context.run();
    }