c++boostcoroutineasio

Boost.Asio default token support causes free function calling ambiguous error


Overview

I wrote some network client class named client based on Boost.Asio. The client uses boost::asio::async_read() internally to read bytes until expected bytes. When I added default completion token supported code to the client, and use the default token, then the compiler reports error call to 'async_read' is ambiguous.

The two ambiguous functions are here:

https://github.com/boostorg/asio/blob/boost-1.84.0/include/boost/asio/impl/read.hpp#L505 https://github.com/boostorg/asio/blob/boost-1.84.0/include/boost/asio/impl/read.hpp#L525

Environment

Linux, x86-64 clang 18.1.0, -std=c++20 Boost 1.84.0

Reproduce code

NOTE: This is only for checking compile error.

godbolt running demo: https://godbolt.org/z/b344jdWzh

#include <string>
#include <boost/asio.hpp>

namespace as = boost::asio;
std::string str;

template <typename NextLayer>
struct client {
    // type aliases
    using next_layer_type = NextLayer;
    using executor_type = typename next_layer_type::executor_type;

    // constructor
    template <typename... Args>
    explicit client(Args&&... args):nl{std::forward<Args>(args)...}{}

    // rebind constructor for default token
    template <typename Other>
    explicit client(client<Other>&& other):nl{std::move(other.nl)} {}

    // accessor
    next_layer_type const& next_layer() const { return nl; };
    next_layer_type& next_layer() { return nl; };
    executor_type get_executor() { return nl.get_executor(); }

    // async_func
    template <
        typename CompletionToken = as::default_completion_token_t<executor_type>
    >
    auto async_read_packet(
        CompletionToken&& token = as::default_completion_token_t<executor_type>{}
    ) {
        return as::async_compose<
            CompletionToken,
            void(boost::system::error_code, std::size_t)
        >(  read_packet_op{ *this }, token );
    }
    // async_func impl
    struct read_packet_op {
        client& c;

        template <typename Self>
        void operator()(Self& self) {
#if 1       
            // calling free function causes error: call to 'async_read' is ambiguous
            as::async_read(
                c.next_layer(),
                as::buffer(str),
                std::move(self)
            );
#else
            // calling member function, no error
            c.next_layer().async_read_some(
                as::buffer(str),
                std::move(self)
            );
#endif
        }

        template <typename Self>
        void operator()(Self& self, boost::system::error_code ec, std::size_t size) {
            self.complete(ec, size);
        }
    };

    // rebind for default token
    template <typename Executor1>
    struct rebind_executor {
        using other = client<
            typename NextLayer::template rebind_executor<Executor1>::other
        >;
    };

    // member variables
    next_layer_type nl;
};

as::awaitable<void> coro_test(auto& c) {
    auto size = co_await c.async_read_packet(as::use_awaitable);
    (void)size;
}
as::awaitable<void> coro_test_default(auto& c) {
    auto size = co_await c.async_read_packet();
    (void)size;
}

int main() {
    using tcp = as::basic_stream_socket<as::ip::tcp, as::any_io_executor>;
    as::io_context ioc;
    {
        // no default token version
        client<tcp> c{ioc.get_executor()};
        as::co_spawn(
            c.get_executor(),
            coro_test(c),
            as::detached
        );
    }
    {
        // default token version
    using default_token = boost::asio::as_tuple_t<boost::asio::use_awaitable_t<>>;
    using def_client = default_token::as_default_on_t<client<tcp>>;
        def_client c{ioc.get_executor()};
        // auto c{as::use_awaitable.as_default_on(client<tcp>{ioc.get_executor()})};
        as::co_spawn(
            c.get_executor(),
            coro_test_default(c),
            as::detached
        );
    }
    ioc.run();
}

What I tried

I replaced the free function version of async_read() with the member function async_read_some(). Then compile error doesn't happen.

You can see the result by replacing #if 1 With #if 0

        template <typename Self>
        void operator()(Self& self) {
#if 1       
            // calling free function causes error: call to 'async_read' is ambiguous
            as::async_read(
                c.next_layer(),
                as::buffer(str),
                std::move(self)
            );
#else
            // calling member function, no error
            c.next_layer().async_read_some(
                as::buffer(str),
                std::move(self)
            );
#endif
        }

So I guess that it is something free function related issue. Is there any way to solve that?


Solution

  • Yes, this is an issue. I've also run into it on occasion. It has nothing to with "free function". Instead it has to do with the presence of other overloads that become ambiguous when letting the compiler deduce the defaulted completion token.

    Note: the defaults for ReadToken are declared in the forward declaring header asio/read.hpp, as opposed to the definition location the compiler reports, asio/impl/read.hpp

    In most cases I've been able to disambiguate by explicitly stating the token:

    asio::async_read(s, b, asio::transfer_all(), asio::deferred);
    

    In generic code you can achieve it by separating the Token deduction from the overload resolution:

    using Token = asio::default_completion_token_t<decltype(s)::executor_type>;
    asio::async_read(s, b, asio::transfer_all(), Token());
    

    Here's a minimized example: https://godbolt.org/z/36zjdbj8x

    Reporting The Issue

    Perhaps the minimal example is a little too minimal. Other operations seemingly suffer from the inverse situation: explicitly specifying the token makes overload resolution fail, for that purpose consider the following example that also exercises asio::async_connect:

    Live On Compiler Explorer

    #include <boost/asio.hpp>
    #include <boost/core/ignore_unused.hpp>
    namespace asio = boost::asio;
    using asio::ip::tcp;
    
    int main() {
        auto x = asio::system_executor{};
        auto r = asio::deferred.as_default_on(tcp::resolver{x});
        auto s = asio::deferred.as_default_on(tcp::socket{x});
    
        asio::streambuf b;
        using Token = asio::default_completion_token_t<decltype(s)::executor_type>;
    
        // auto op1 = asio::async_connect(s, r.resolve("", "8989"), Token());// BROKEN
        auto op1 = asio::async_connect(s, r.resolve("", "8989")); // Workaround
    
        // but conversely:
    
        // auto op2 = asio::async_read(s, b, asio::transfer_all()); // BROKEN
        auto op2 = asio::async_read(s, b, asio::transfer_all(), Token()); // Workaround
    
        boost::ignore_unused(op1, op2);
    }