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