boostasio

What's going on with asio::async_compose?


The following code snippet produces a compilation error:

error: deduced type 'void' for 'err' is incomplete.

What I don't understand is this. Shouldn't err be of type error_code?

#include <boost/asio/co_spawn.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/core/ignore_unused.hpp>

#include <cstdio>
#include <format>
#include <utility>

namespace asio = boost::asio;
namespace sys = boost::system;

template<class... Args>
void debugln(std::FILE* stream, std::format_string<Args...> fmt, Args&&... args)
{
    std::string s = std::format(fmt, std::forward<Args>(args)...);
    std::fprintf(stream, "%s\n", s.c_str());
}

template<class... Args>
void debugln(std::format_string<Args...> fmt, Args&&... args)
{
    debugln(stdout, fmt, std::forward<Args>(args)...);
}

namespace detail {
struct connect_op
{
    std::string_view host_;
    std::string_view port_;
    asio::ip::tcp::socket* sock_;
    asio::ip::tcp::resolver resolv_;

    connect_op(std::string_view host, std::string_view port, asio::ip::tcp::socket* tcp_sock)
        : resolv_(tcp_sock->get_executor())
    {
        host_ = host;
        port_ = port;
        sock_ = tcp_sock;
    }

    // This overload will be called after the async_resolve completes
    template<typename Self>
    void operator()(Self& self, sys::error_code ec, asio::ip::tcp::resolver::results_type endpoints)
    {
        if (ec) {
            debugln("(connect_op) async_resolve failed: {}", ec.message());
            return self.complete(ec);
        } else {
            debugln("(connect_op) async_resolve succeeded: {}", endpoints.size());
            asio::async_connect(*sock_, std::move(endpoints), std::move(self));
        }
    }

    // This overload will be called after the async_connect completes
    template<class Self>
    void operator()(
        Self& self,
        sys::error_code ec,
        const asio::ip::tcp::endpoint& selected_endpoint
    )
    {
        if (ec) {
            debugln("(connect_op) async_connect failed: {}", ec.message());
        } else {
            debugln("(connect_op) async connected to {}", selected_endpoint.address().to_string());
        }

        self.complete(ec);
    }

    // This overload will be used for the initiation completion handler
    // `async_compose` will cause the implementation (in this case, `connect_op`) to be called once
    // with no arguments (other than self) during initiation
    template<class Self>
    void operator()(Self& self, sys::error_code ec = {})
    {
        boost::ignore_unused(ec);
        resolv_.async_resolve(host_, port_, std::move(self));
    }
};
} // namespace detail

class tcp_connection
{
public:
    /// Executor type.
    using executor_type = asio::any_io_executor;

    tcp_connection(std::string_view host, std::string_view port, executor_type ex)
        : host_(host), port_(port), sock_(ex, asio::ip::tcp::v4())
    {
    }

    tcp_connection(std::string_view host, std::string_view port, asio::io_context& ioc)
        : tcp_connection(host, port, ioc.get_executor())
    {
    }

    /// Returns the underlying executor.
    executor_type get_executor() noexcept { return sock_.get_executor(); }

    template<typename CompletionToken = asio::deferred_t>
    auto async_connect(CompletionToken&& token = {})
    {
        return asio::async_compose<CompletionToken, void(sys::error_code)>(
            detail::connect_op(host_, port_, &sock_),
            token,
            sock_.get_executor()
        );
    }

private:
    std::string host_;
    std::string port_;
    asio::ip::tcp::socket sock_;
};

asio::awaitable<void> co_main(std::string_view host, std::string_view port)
{
    auto exector = co_await asio::this_coro::executor;

    auto conn = tcp_connection(host, port, exector);

    // ERROR: deduced type 'void' for 'err' is incomplete
    // why err is void?
    auto err = co_await conn.async_connect();

    co_return;
}

int main(int argc, char* argv[])
{
    if (argc < 3) {
        std::printf("Usage: %s <host> <port>\n", argv[0]);
        return 1;
    }

    std::string host = argv[1];
    std::string port = argv[2];

    try {
        asio::io_context ioc;
        asio::co_spawn(ioc, co_main(host, port), [](std::exception_ptr p) {
            if (p) {
                std::rethrow_exception(p);
            }
        });
        ioc.run();
        return 0;
    } catch (std::exception const& e) {
        debugln("(main) {}", e.what());
        return 1;
    }
}

Solution

  • If you want to use deferred as the completion token, coroutines will handle the error by throwing exceptions.

    You can confuse yourself a little by demonstrating this by replacing sys::error_code with std::error_code and see how it suddenly becomes an actual return-value!

    The simple solution is to use

    Review Notes

    I'd advise against moving the resolver each handler invocation, because it implies that you're moving it during the async_resolve operation as well. That is liable to create a race condition because async resolving is emulated with an internal thread (Implementation Notes).

    I see no reason to not use base/member initializers for the op members. That also gives you the way to avoid mucking with a raw pointer that should be a reference.

    Continuing

    What's also weird to me, is that the compiler doesn't give any template instantiation trace (GCC 15.2.0):

    [ 11%] Building CXX object CMakeFiles/sotest.dir/test.cpp.o
    /home/sehe/Projects/stackoverflow/test.cpp: In function ‘boost::asio::awaitable<void> co_main(std::string_view, std::string_view)’:
    /home/sehe/Projects/stackoverflow/test.cpp:86:10: error: deduced type ‘void’ for ‘err’ is incomplete
       86 |     auto err = co_await conn.async_connect(asio::use_awaitable);
          |          ^~~
    

    So let's try Clang++ (20.1.8) instead... Sadly, it's even terser in its output:

    || [ 11%] Building CXX object CMakeFiles/sotest.dir/test.cpp.o
    test.cpp|86 col 10| error: variable has incomplete type 'void'
    ||    86 |     auto err = co_await conn.async_connect(asio::use_awaitable);
    ||       |          ^
    || 1 error generated.
    

    Fixups

    Showing some more ideas on the fly - feel free to ignore what you don't need/like:

    Live On Coliru

    #include <boost/asio.hpp>
    #include <format>
    
    namespace asio = boost::asio;
    namespace sys  = boost::system;
    using asio::ip::tcp;
    using sys::error_code;
    
    template <> struct std::formatter<tcp::endpoint> : std::formatter<std::string> {
        auto format(tcp::endpoint const& ep, auto& ctx) const {
            return std::format_to(ctx.out(), "{}:{}", ep.address().to_string(), ep.port());
        }
    };
    
    template <class... Args> void debugln(std::FILE* stream, std::format_string<Args...> fmt, Args&&... args) {
        std::string s = std::format(fmt, std::forward<Args>(args)...);
        std::fprintf(stream, "%s\n", s.c_str());
    }
    
    template <class... Args> void debugln(std::format_string<Args...> fmt, Args&&... args) {
        debugln(stdout, fmt, std::forward<Args>(args)...);
    }
    
    namespace detail {
        struct connect_op {
            tcp::socket&                   sock_;
            std::string_view               host_, port_;
            std::unique_ptr<tcp::resolver> resolv_ = std::make_unique<tcp::resolver>(sock_.get_executor());
    
            explicit connect_op(std::string_view host, std::string_view port, tcp::socket& tcp_sock)
                : sock_(tcp_sock), host_(host), port_(port) {}
    
            // This overload will be called after the async_resolve completes
            void operator()(auto& self, error_code const& ec, tcp::resolver::results_type endpoints) const {
                if (ec) {
                    debugln("(connect_op) async_resolve failed: {}", ec.message());
                    return std::move(self).complete(ec);
                } else {
                    debugln("(connect_op) async_resolve succeeded: {}", endpoints.size());
                    async_connect(sock_, std::move(endpoints), std::move(self));
                }
            }
    
            // This overload will be called after the async_connect completes
            void operator()(auto& self, error_code const& ec, tcp::endpoint const& selected_endpoint) const {
                if (ec) {
                    debugln("(connect_op) async_connect failed: {}", ec.message());
                } else {
                    debugln("(connect_op) async connected to {}", selected_endpoint);
                }
    
                std::move(self).complete(ec);
            }
    
            void operator()(auto& self) const { // entry point, after initiation
                resolv_->async_resolve(host_, port_, std::move(self));
            }
        };
    } // namespace detail
    
    class tcp_connection {
      public:
        using executor_type = asio::any_io_executor;
    
        tcp_connection(std::string_view host, std::string_view port, executor_type ex)
            : ex_(std::move(ex)), host_(host), port_(port) {}
    
        executor_type get_executor() const noexcept { return ex_; }
    
        template <asio::completion_token_for<void(error_code)> Token = asio::deferred_t>
        auto async_connect(Token&& token = {}) {
            return asio::async_compose<Token, void(error_code)>( //
                detail::connect_op(host_, port_, sock_),         //
                token, sock_ /*.get_executor()*/);
        }
    
      private:
        executor_type ex_;
        std::string   host_, port_;
        tcp::socket   sock_{ex_ /*, tcp::v4()*/};
    };
    
    asio::awaitable<void> co_main(std::string_view host, std::string_view port) {
        auto exector = co_await asio::this_coro::executor;
        auto conn    = tcp_connection(host, port, exector);
    
        // 1. either:
        {
            auto [err] = co_await conn.async_connect(asio::as_tuple);
            debugln("err: {}", err.message());
        }
    
        // 2. or:
        try {
            co_await conn.async_connect(asio::as_tuple);
        } catch (sys::system_error const& se) {
            debugln(stderr, "Error: {}", se.code().message());
        } catch (std::exception const& e) {
            debugln(stderr, "Exception: {}", e.what());
        }
    
        // 3. or even:
        {
            error_code ec;
            auto       token = asio::redirect_error(ec);
    
            co_await conn.async_connect(token);
            debugln("ec: {}", ec.message());
        }
    }
    
    int main(int argc, char** argv) try {
        if (argc < 3) {
            std::printf("Usage: %s <host> <port>\n", argv[0]);
            return 1;
        }
    
        std::string_view host = argv[1];
        std::string_view port = argv[2];
    
        asio::io_context ioc;
        co_spawn(ioc, co_main(host, port), [](std::exception_ptr p) {
            if (p) {
                std::rethrow_exception(p);
            }
        });
        ioc.run();
    } catch (std::exception const& e) {
        debugln(stderr, "(main) {}", e.what());
        return 1;
    }
    

    Printing, locally:

    enter image description here