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;
}
}
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_codewithstd::error_codeand see how it suddenly becomes an actual return-value!
The simple solution is to use
asio::as_tupleI'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.
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.
Showing some more ideas on the fly - feel free to ignore what you don't need/like:
#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: