c++boost-asioboost-beast

Declaration of a variable without yet knowing the deduced return type that would otherwise be assinged via auto var =


Using Boost.Asio in C++23, how can I declare the below code's endpoints variable outside of the try-catch scope in which it is assigned a value? If it was declared inside the try, the assignment would become initialization, hence its type could be deduced via auto. However, having the declaration outside the try separates the declaration from the desired value, hence auto cannot be used. The desired value in this case comes from co_awaiting resolver.async_resolve().

One thing I like about c++ is the predictability of the return types. However with asio, looking at the source code, I cannot figure it out. In this particular case, a specific answer would be helpful, but I would also appreciate recommendations how to do figure out actual types based on the given context, with asio or other heavy template based libraries that deduce the return type at compile time.

boost::beast::error_code ec;

// How to put `endpoints` to scope outside try-catch?
// auto endpoints = ...?
try {
    // Before
    // auto const endpoints = co_await resolver.async_resolve(host, port);
    // After
    endpoints = co_await resolver.async_resolve(host, port);
}
catch (const std::exception& e) {
    ec = error_code(asio::error::host_not_found);
    co_return ec;
}

// use `endpoints` without putting the code into the `try` block
auto endpoint = co_await beast::get_lowest_layer(stream).async_connect(endpoints, asio::redirect_error(asio::use_awaitable, ec));

Solution

  • First

    You can use the decltype trick on the awaitable<>:

    using T = decltype(resolver.async_resolve(host, port, asio::use_awaitable))::value_type;
    T endpoints;
    

    Second

    Of course, you likely know the exact type of resolver so you can simply use that knowledge:

    auto              ex = co_await asio::this_coro::executor;
    tcp::resolver     resolver(ex);
    beast::tcp_stream stream(ex);
    
    tcp::resolver::results_type endpoints;
    

    Best?

    However, you could greatly simplify. You already use ec. Just go all the way?

    Live On Coliru

    auto              ex = co_await asio::this_coro::executor;
    tcp::resolver     resolver(ex);
    beast::tcp_stream stream(ex);
    
    beast::error_code ec;
    auto token = asio::redirect_error(ec);
    
    auto endpoints = co_await resolver.async_resolve(host, port, token);
    
    if (ec)
        co_return asio::error::host_not_found; // co_return ec; ?
    
    auto endpoint = co_await get_lowest_layer(stream).async_connect(endpoints, token);
    if (!ec)
        std::cout << "Connected to " << endpoint << std::endl;
    

    Of course, I'd personally always favor exception transparency:

    Live On Coliru

    asio::awaitable<boost::beast::error_code> run_server(std::string host, std::string port) try {
        auto          ex = co_await asio::this_coro::executor;
        tcp::resolver resolver(ex);
    
        auto eps = co_await resolver.async_resolve(host, port);
    
        beast::tcp_stream stream(ex);
        auto ep  = co_await get_lowest_layer(stream).async_connect(eps);
    
        std::cout << "Connected to " << ep << std::endl;
    } catch (beast::system_error const& se) {
        co_return se.code();
    }
    

    Or, even more to the point, exception neutrality:

    Live On Coliru

    asio::awaitable<void> run_server(std::string host, std::string port) {
        auto          ex = co_await asio::this_coro::executor;
        tcp::resolver resolver(ex);
    
        auto eps = co_await resolver.async_resolve(host, port);
    
        beast::tcp_stream stream(ex);
        auto ep  = co_await get_lowest_layer(stream).async_connect(eps);
    
        std::cout << "Connected to " << ep << std::endl;
    }