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
Linux, x86-64 clang 18.1.0, -std=c++20 Boost 1.84.0
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();
}
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?
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 headerasio/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
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
:
#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);
}