Skip to content

Commit 41fc681

Browse files
committed
Add range-based connect composed operation
Free functions in boost/corosio/connect.hpp that try each endpoint in a range (or iterator pair) until one connects, with optional connect-condition predicate. Mirrors Boost.Asio's async_connect semantics adapted to corosio's coroutine model.
1 parent 3fc8c97 commit 41fc681

5 files changed

Lines changed: 827 additions & 0 deletions

File tree

doc/modules/ROOT/pages/4.guide/4d.sockets.adoc

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,78 @@ For simpler code when errors are fatal:
136136
(co_await s.connect(endpoint)).value(); // Throws on error
137137
----
138138

139+
=== Range-Based Connect
140+
141+
When connecting to a hostname, the resolver may return multiple endpoints
142+
(IPv4, IPv6, multiple A records). The free function `corosio::connect()` tries
143+
each in order, returning on the first success:
144+
145+
[source,cpp]
146+
----
147+
#include <boost/corosio/connect.hpp>
148+
149+
corosio::resolver r(ioc);
150+
auto [rec, results] = co_await r.resolve("www.boost.org", "80");
151+
if (rec)
152+
co_return;
153+
154+
corosio::tcp_socket s(ioc);
155+
auto [cec, ep] = co_await corosio::connect(s, results);
156+
if (cec)
157+
co_return;
158+
// `ep` is the endpoint that accepted the connection.
159+
----
160+
161+
Between attempts the socket is closed so the next `connect()` auto-opens with
162+
the correct address family. This lets a single call try IPv4 and IPv6
163+
candidates transparently.
164+
165+
The signature is generic over any range whose elements convert to the
166+
socket's endpoint type:
167+
168+
[source,cpp]
169+
----
170+
template<class Socket, std::ranges::input_range Range>
171+
requires std::convertible_to<
172+
std::ranges::range_reference_t<Range>,
173+
typename Socket::endpoint_type>
174+
capy::task<capy::io_result<typename Socket::endpoint_type>>
175+
connect(Socket& s, Range endpoints);
176+
----
177+
178+
On success, returns the connected endpoint. On all-fail, returns the error
179+
from the last attempt. On empty range (or when a connect condition rejects
180+
every candidate), returns `std::errc::no_such_device_or_address`.
181+
182+
==== Filtering Candidates
183+
184+
A second overload accepts a predicate invoked as `cond(last_ec, ep)` before
185+
each attempt. Returning `false` skips the candidate:
186+
187+
[source,cpp]
188+
----
189+
auto [ec, ep] = co_await corosio::connect(
190+
s,
191+
results,
192+
[](std::error_code const&, corosio::endpoint const& e) {
193+
return e.is_v4(); // IPv4 only.
194+
});
195+
----
196+
197+
==== Iterator Overload
198+
199+
An iterator-pair overload returns the iterator to the successful endpoint on
200+
success, or `end` on failure:
201+
202+
[source,cpp]
203+
----
204+
auto [ec, it] = co_await corosio::connect(s, v.begin(), v.end());
205+
if (!ec)
206+
std::cout << "connected to index " << (it - v.begin()) << "\n";
207+
----
208+
209+
Both overloads accept an optional connect condition as a trailing argument.
210+
139211
== Reading Data
140212

141213
=== read_some()

include/boost/corosio/connect.hpp

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
//
2+
// Copyright (c) 2026 Michael Vandeberg
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/corosio
8+
//
9+
10+
#ifndef BOOST_COROSIO_CONNECT_HPP
11+
#define BOOST_COROSIO_CONNECT_HPP
12+
13+
#include <boost/corosio/detail/config.hpp>
14+
15+
#include <boost/capy/cond.hpp>
16+
#include <boost/capy/io_result.hpp>
17+
#include <boost/capy/task.hpp>
18+
19+
#include <concepts>
20+
#include <iterator>
21+
#include <ranges>
22+
#include <system_error>
23+
#include <utility>
24+
25+
/*
26+
Range-based composed connect operation.
27+
28+
These free functions try each endpoint in a range (or iterator pair)
29+
in order, returning on the first successful connect. Between attempts
30+
the socket is closed so that the next attempt can auto-open with the
31+
correct address family (e.g. going from IPv4 to IPv6 candidates).
32+
33+
The iteration semantics follow Boost.Asio's range/iterator async_connect:
34+
on success, the successful endpoint (or its iterator) is returned; on
35+
all-fail, the last attempt's error code is returned; on an empty range
36+
(or when a connect_condition rejects every candidate),
37+
std::errc::no_such_device_or_address is returned, matching the error
38+
the resolver uses for "no results" in posix_resolver_service.
39+
40+
The operation is a plain coroutine; cancellation is propagated to the
41+
inner per-endpoint connect via the affine awaitable protocol on io_env.
42+
*/
43+
44+
namespace boost::corosio {
45+
46+
namespace detail {
47+
48+
/* Always-true connect condition used by the overloads that take no
49+
user-supplied predicate. Kept at namespace-detail scope so it has a
50+
stable linkage name across translation units. */
51+
struct default_connect_condition
52+
{
53+
template<class Endpoint>
54+
bool operator()(std::error_code const&, Endpoint const&) const noexcept
55+
{
56+
return true;
57+
}
58+
};
59+
60+
} // namespace detail
61+
62+
/* Forward declarations so the non-condition overloads can delegate
63+
to the condition overloads via qualified lookup (qualified calls
64+
bind to the overload set visible at definition, not instantiation). */
65+
66+
template<class Socket, std::ranges::input_range Range, class ConnectCondition>
67+
requires std::convertible_to<
68+
std::ranges::range_reference_t<Range>,
69+
typename Socket::endpoint_type> &&
70+
std::predicate<
71+
ConnectCondition&,
72+
std::error_code const&,
73+
typename Socket::endpoint_type const&>
74+
capy::task<capy::io_result<typename Socket::endpoint_type>>
75+
connect(Socket& s, Range endpoints, ConnectCondition cond);
76+
77+
template<class Socket, std::input_iterator Iter, class ConnectCondition>
78+
requires std::convertible_to<
79+
std::iter_reference_t<Iter>,
80+
typename Socket::endpoint_type> &&
81+
std::predicate<
82+
ConnectCondition&,
83+
std::error_code const&,
84+
typename Socket::endpoint_type const&>
85+
capy::task<capy::io_result<Iter>>
86+
connect(Socket& s, Iter begin, Iter end, ConnectCondition cond);
87+
88+
/** Asynchronously connect a socket by trying each endpoint in a range.
89+
90+
Each candidate is tried in order. Before each attempt the socket is
91+
closed (so the next `connect` auto-opens with the candidate's
92+
address family). On first successful connect, the operation
93+
completes with the connected endpoint.
94+
95+
@par Cancellation
96+
Supports cancellation via the affine awaitable protocol. If a
97+
per-endpoint connect completes with `capy::cond::canceled` the
98+
operation completes immediately with that error and does not try
99+
further endpoints.
100+
101+
@param s The socket to connect. Must have a `connect(endpoint)`
102+
member returning an awaitable, plus `close()` and `is_open()`.
103+
If the socket is already open, it will be closed before the
104+
first attempt.
105+
@param endpoints A range of candidate endpoints. Taken by value
106+
so temporaries (e.g. `resolver_results` returned from
107+
`resolver::resolve`) remain alive for the coroutine's lifetime.
108+
109+
@return An awaitable completing with
110+
`capy::io_result<typename Socket::endpoint_type>`:
111+
- on success: default error_code and the connected endpoint;
112+
- on failure of all attempts: the error from the last attempt
113+
and a default-constructed endpoint;
114+
- on empty range: `std::errc::no_such_device_or_address` and a
115+
default-constructed endpoint.
116+
117+
@throws std::system_error if auto-opening the socket fails during
118+
an attempt (inherits the contract of `Socket::connect`).
119+
120+
@par Example
121+
@code
122+
resolver r(ioc);
123+
auto [rec, results] = co_await r.resolve("www.boost.org", "80");
124+
if (rec) co_return;
125+
tcp_socket s(ioc);
126+
auto [cec, ep] = co_await corosio::connect(s, results);
127+
@endcode
128+
*/
129+
template<class Socket, std::ranges::input_range Range>
130+
requires std::convertible_to<
131+
std::ranges::range_reference_t<Range>,
132+
typename Socket::endpoint_type>
133+
capy::task<capy::io_result<typename Socket::endpoint_type>>
134+
connect(Socket& s, Range endpoints)
135+
{
136+
return corosio::connect(
137+
s, std::move(endpoints), detail::default_connect_condition{});
138+
}
139+
140+
/** Asynchronously connect a socket by trying each endpoint in a range,
141+
filtered by a user-supplied condition.
142+
143+
For each candidate the condition is invoked as
144+
`cond(last_ec, ep)` where `last_ec` is the error from the most
145+
recent attempt (default-constructed before the first attempt). If
146+
the condition returns `false` the candidate is skipped; otherwise a
147+
connect is attempted.
148+
149+
@param s The socket to connect. See the non-condition overload for
150+
requirements.
151+
@param endpoints A range of candidate endpoints.
152+
@param cond A predicate invocable with
153+
`(std::error_code const&, typename Socket::endpoint_type const&)`
154+
returning a value contextually convertible to `bool`.
155+
156+
@return Same as the non-condition overload. If every candidate is
157+
rejected, completes with `std::errc::no_such_device_or_address`.
158+
159+
@throws std::system_error if auto-opening the socket fails.
160+
*/
161+
template<class Socket, std::ranges::input_range Range, class ConnectCondition>
162+
requires std::convertible_to<
163+
std::ranges::range_reference_t<Range>,
164+
typename Socket::endpoint_type> &&
165+
std::predicate<
166+
ConnectCondition&,
167+
std::error_code const&,
168+
typename Socket::endpoint_type const&>
169+
capy::task<capy::io_result<typename Socket::endpoint_type>>
170+
connect(Socket& s, Range endpoints, ConnectCondition cond)
171+
{
172+
using endpoint_type = typename Socket::endpoint_type;
173+
174+
std::error_code last_ec;
175+
176+
for (auto&& e : endpoints)
177+
{
178+
endpoint_type ep = e;
179+
180+
if (!cond(static_cast<std::error_code const&>(last_ec),
181+
static_cast<endpoint_type const&>(ep)))
182+
continue;
183+
184+
if (s.is_open())
185+
s.close();
186+
187+
auto [ec] = co_await s.connect(ep);
188+
189+
if (!ec)
190+
co_return {std::error_code{}, std::move(ep)};
191+
192+
if (ec == capy::cond::canceled)
193+
co_return {ec, endpoint_type{}};
194+
195+
last_ec = ec;
196+
}
197+
198+
if (!last_ec)
199+
last_ec = std::make_error_code(std::errc::no_such_device_or_address);
200+
201+
co_return {last_ec, endpoint_type{}};
202+
}
203+
204+
/** Asynchronously connect a socket by trying each endpoint in an
205+
iterator range.
206+
207+
Behaves like the range overload, except the return value carries
208+
the iterator to the successfully connected endpoint on success, or
209+
`end` on failure. This mirrors Boost.Asio's iterator-based
210+
`async_connect`.
211+
212+
@param s The socket to connect.
213+
@param begin The first candidate.
214+
@param end One past the last candidate.
215+
216+
@return An awaitable completing with `capy::io_result<Iter>`:
217+
- on success: default error_code and the iterator of the
218+
successful endpoint;
219+
- on failure of all attempts: the error from the last attempt
220+
and `end`;
221+
- on empty range: `std::errc::no_such_device_or_address` and
222+
`end`.
223+
224+
@throws std::system_error if auto-opening the socket fails.
225+
*/
226+
template<class Socket, std::input_iterator Iter>
227+
requires std::convertible_to<
228+
std::iter_reference_t<Iter>,
229+
typename Socket::endpoint_type>
230+
capy::task<capy::io_result<Iter>>
231+
connect(Socket& s, Iter begin, Iter end)
232+
{
233+
return corosio::connect(
234+
s,
235+
std::move(begin),
236+
std::move(end),
237+
detail::default_connect_condition{});
238+
}
239+
240+
/** Asynchronously connect a socket by trying each endpoint in an
241+
iterator range, filtered by a user-supplied condition.
242+
243+
@param s The socket to connect.
244+
@param begin The first candidate.
245+
@param end One past the last candidate.
246+
@param cond A predicate invocable with
247+
`(std::error_code const&, typename Socket::endpoint_type const&)`.
248+
249+
@return Same as the plain iterator overload. If every candidate is
250+
rejected, completes with `std::errc::no_such_device_or_address`.
251+
252+
@throws std::system_error if auto-opening the socket fails.
253+
*/
254+
template<class Socket, std::input_iterator Iter, class ConnectCondition>
255+
requires std::convertible_to<
256+
std::iter_reference_t<Iter>,
257+
typename Socket::endpoint_type> &&
258+
std::predicate<
259+
ConnectCondition&,
260+
std::error_code const&,
261+
typename Socket::endpoint_type const&>
262+
capy::task<capy::io_result<Iter>>
263+
connect(Socket& s, Iter begin, Iter end, ConnectCondition cond)
264+
{
265+
using endpoint_type = typename Socket::endpoint_type;
266+
267+
std::error_code last_ec;
268+
269+
for (Iter it = begin; it != end; ++it)
270+
{
271+
endpoint_type ep = *it;
272+
273+
if (!cond(static_cast<std::error_code const&>(last_ec),
274+
static_cast<endpoint_type const&>(ep)))
275+
continue;
276+
277+
if (s.is_open())
278+
s.close();
279+
280+
auto [ec] = co_await s.connect(ep);
281+
282+
if (!ec)
283+
co_return {std::error_code{}, std::move(it)};
284+
285+
if (ec == capy::cond::canceled)
286+
co_return {ec, std::move(end)};
287+
288+
last_ec = ec;
289+
}
290+
291+
if (!last_ec)
292+
last_ec = std::make_error_code(std::errc::no_such_device_or_address);
293+
294+
co_return {last_ec, std::move(end)};
295+
}
296+
297+
} // namespace boost::corosio
298+
299+
#endif

include/boost/corosio/local_stream_socket.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ namespace boost::corosio {
7777
class BOOST_COROSIO_DECL local_stream_socket : public io_stream
7878
{
7979
public:
80+
/// The endpoint type used by this socket.
81+
using endpoint_type = corosio::local_endpoint;
82+
8083
using shutdown_type = corosio::shutdown_type;
8184
using enum corosio::shutdown_type;
8285

include/boost/corosio/tcp_socket.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ namespace boost::corosio {
7878
class BOOST_COROSIO_DECL tcp_socket : public io_stream
7979
{
8080
public:
81+
/// The endpoint type used by this socket.
82+
using endpoint_type = corosio::endpoint;
83+
8184
using shutdown_type = corosio::shutdown_type;
8285
using enum corosio::shutdown_type;
8386

0 commit comments

Comments
 (0)