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 +
    @note The socket is closed and re-opened before each attempt, so
 
118 +
        any socket options set by the caller (e.g. `no_delay`,
 
119 +
        `reuse_address`) are lost. Apply options after this operation
 
120 +
        completes.
 
121 +

 
122 +
    @throws std::system_error if auto-opening the socket fails during
 
123 +
        an attempt (inherits the contract of `Socket::connect`).
 
124 +

 
125 +
    @par Example
 
126 +
    @code
 
127 +
    resolver r(ioc);
 
128 +
    auto [rec, results] = co_await r.resolve("www.boost.org", "80");
 
129 +
    if (rec) co_return;
 
130 +
    tcp_socket s(ioc);
 
131 +
    auto [cec, ep] = co_await corosio::connect(s, results);
 
132 +
    @endcode
 
133 +
*/
 
134 +
template<class Socket, std::ranges::input_range Range>
 
135 +
    requires std::convertible_to<
 
136 +
        std::ranges::range_reference_t<Range>,
 
137 +
        typename Socket::endpoint_type>
 
138 +
capy::task<capy::io_result<typename Socket::endpoint_type>>
 
139 +
connect(Socket& s, Range endpoints)
 
140 +
{
 
141 +
    return corosio::connect(
 
142 +
        s, std::move(endpoints), detail::default_connect_condition{});
 
143 +
}
 
144 +

 
145 +
/** Asynchronously connect a socket by trying each endpoint in a range,
 
146 +
    filtered by a user-supplied condition.
 
147 +

 
148 +
    For each candidate the condition is invoked as
 
149 +
    `cond(last_ec, ep)` where `last_ec` is the error from the most
 
150 +
    recent attempt (default-constructed before the first attempt). If
 
151 +
    the condition returns `false` the candidate is skipped; otherwise a
 
152 +
    connect is attempted.
 
153 +

 
154 +
    @param s The socket to connect. See the non-condition overload for
 
155 +
        requirements.
 
156 +
    @param endpoints A range of candidate endpoints.
 
157 +
    @param cond A predicate invocable with
 
158 +
        `(std::error_code const&, typename Socket::endpoint_type const&)`
 
159 +
        returning a value contextually convertible to `bool`.
 
160 +

 
161 +
    @return Same as the non-condition overload. If every candidate is
 
162 +
        rejected, completes with `std::errc::no_such_device_or_address`.
 
163 +

 
164 +
    @throws std::system_error if auto-opening the socket fails.
 
165 +
*/
 
166 +
template<class Socket, std::ranges::input_range Range, class ConnectCondition>
 
167 +
    requires std::convertible_to<
 
168 +
                 std::ranges::range_reference_t<Range>,
 
169 +
                 typename Socket::endpoint_type> &&
 
170 +
    std::predicate<
 
171 +
                 ConnectCondition&,
 
172 +
                 std::error_code const&,
 
173 +
                 typename Socket::endpoint_type const&>
 
174 +
capy::task<capy::io_result<typename Socket::endpoint_type>>
 
175 +
connect(Socket& s, Range endpoints, ConnectCondition cond)
 
176 +
{
 
177 +
    using endpoint_type = typename Socket::endpoint_type;
 
178 +

 
179 +
    std::error_code last_ec;
 
180 +

 
181 +
    for (auto&& e : endpoints)
 
182 +
    {
 
183 +
        endpoint_type ep = e;
 
184 +

 
185 +
        if (!cond(static_cast<std::error_code const&>(last_ec),
 
186 +
                  static_cast<endpoint_type const&>(ep)))
 
187 +
            continue;
 
188 +

 
189 +
        if (s.is_open())
 
190 +
            s.close();
 
191 +

 
192 +
        auto [ec] = co_await s.connect(ep);
 
193 +

 
194 +
        if (!ec)
 
195 +
            co_return {std::error_code{}, std::move(ep)};
 
196 +

 
197 +
        if (ec == capy::cond::canceled)
 
198 +
            co_return {ec, endpoint_type{}};
 
199 +

 
200 +
        last_ec = ec;
 
201 +
    }
 
202 +

 
203 +
    if (!last_ec)
 
204 +
        last_ec = std::make_error_code(std::errc::no_such_device_or_address);
 
205 +

 
206 +
    co_return {last_ec, endpoint_type{}};
 
207 +
}
 
208 +

 
209 +
/** Asynchronously connect a socket by trying each endpoint in an
 
210 +
    iterator range.
 
211 +

 
212 +
    Behaves like the range overload, except the return value carries
 
213 +
    the iterator to the successfully connected endpoint on success, or
 
214 +
    `end` on failure. This mirrors Boost.Asio's iterator-based
 
215 +
    `async_connect`.
 
216 +

 
217 +
    @param s The socket to connect.
 
218 +
    @param begin The first candidate.
 
219 +
    @param end One past the last candidate.
 
220 +

 
221 +
    @return An awaitable completing with `capy::io_result<Iter>`:
 
222 +
        - on success: default error_code and the iterator of the
 
223 +
          successful endpoint;
 
224 +
        - on failure of all attempts: the error from the last attempt
 
225 +
          and `end`;
 
226 +
        - on empty range: `std::errc::no_such_device_or_address` and
 
227 +
          `end`.
 
228 +

 
229 +
    @throws std::system_error if auto-opening the socket fails.
 
230 +
*/
 
231 +
template<class Socket, std::input_iterator Iter>
 
232 +
    requires std::convertible_to<
 
233 +
        std::iter_reference_t<Iter>,
 
234 +
        typename Socket::endpoint_type>
 
235 +
capy::task<capy::io_result<Iter>>
 
236 +
connect(Socket& s, Iter begin, Iter end)
 
237 +
{
 
238 +
    return corosio::connect(
 
239 +
        s,
 
240 +
        std::move(begin),
 
241 +
        std::move(end),
 
242 +
        detail::default_connect_condition{});
 
243 +
}
 
244 +

 
245 +
/** Asynchronously connect a socket by trying each endpoint in an
 
246 +
    iterator range, filtered by a user-supplied condition.
 
247 +

 
248 +
    @param s The socket to connect.
 
249 +
    @param begin The first candidate.
 
250 +
    @param end One past the last candidate.
 
251 +
    @param cond A predicate invocable with
 
252 +
        `(std::error_code const&, typename Socket::endpoint_type const&)`.
 
253 +

 
254 +
    @return Same as the plain iterator overload. If every candidate is
 
255 +
        rejected, completes with `std::errc::no_such_device_or_address`.
 
256 +

 
257 +
    @throws std::system_error if auto-opening the socket fails.
 
258 +
*/
 
259 +
template<class Socket, std::input_iterator Iter, class ConnectCondition>
 
260 +
    requires std::convertible_to<
 
261 +
                 std::iter_reference_t<Iter>,
 
262 +
                 typename Socket::endpoint_type> &&
 
263 +
    std::predicate<
 
264 +
                 ConnectCondition&,
 
265 +
                 std::error_code const&,
 
266 +
                 typename Socket::endpoint_type const&>
 
267 +
capy::task<capy::io_result<Iter>>
 
268 +
connect(Socket& s, Iter begin, Iter end, ConnectCondition cond)
 
269 +
{
 
270 +
    using endpoint_type = typename Socket::endpoint_type;
 
271 +

 
272 +
    std::error_code last_ec;
 
273 +

 
274 +
    for (Iter it = begin; it != end; ++it)
 
275 +
    {
 
276 +
        endpoint_type ep = *it;
 
277 +

 
278 +
        if (!cond(static_cast<std::error_code const&>(last_ec),
 
279 +
                  static_cast<endpoint_type const&>(ep)))
 
280 +
            continue;
 
281 +

 
282 +
        if (s.is_open())
 
283 +
            s.close();
 
284 +

 
285 +
        auto [ec] = co_await s.connect(ep);
 
286 +

 
287 +
        if (!ec)
 
288 +
            co_return {std::error_code{}, std::move(it)};
 
289 +

 
290 +
        if (ec == capy::cond::canceled)
 
291 +
            co_return {ec, std::move(end)};
 
292 +

 
293 +
        last_ec = ec;
 
294 +
    }
 
295 +

 
296 +
    if (!last_ec)
 
297 +
        last_ec = std::make_error_code(std::errc::no_such_device_or_address);
 
298 +

 
299 +
    co_return {last_ec, std::move(end)};
 
300 +
}
 
301 +

 
302 +
} // namespace boost::corosio
 
303 +

 
304 +
#endif