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