92.31% Lines (12/13) 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 will be closed before the 103   If the socket is already open, it will be 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. `resolver_results` returned from 106   so temporaries (e.g. `resolver_results` returned from
107   `resolver::resolve`) remain alive for the coroutine's lifetime. 107   `resolver::resolve`) remain alive for the coroutine's lifetime.
108   Because the range is owned by the coroutine, passing an lvalue 108   Because the range is owned by the coroutine, passing an lvalue
109   copies it; since `resolver_results` is a 109   copies it; since `resolver_results` is a
110   `std::vector<resolver_entry>`, that is a deep copy of every entry. 110   `std::vector<resolver_entry>`, that is a deep copy of every entry.
111   Pass an rvalue (`std::move(results)`) or use the iterator overload 111   Pass an rvalue (`std::move(results)`) or use the iterator overload
112   (`connect(s, results.begin(), results.end())`) to avoid the copy. 112   (`connect(s, results.begin(), results.end())`) to avoid the copy.
113   113  
114   @return An awaitable completing with 114   @return An awaitable completing with
115   `capy::io_result<typename Socket::endpoint_type>`: 115   `capy::io_result<typename Socket::endpoint_type>`:
116   - on success: default error_code and the connected endpoint; 116   - on success: default error_code and the connected endpoint;
117   - on failure of all attempts: the error from the last attempt 117   - on failure of all attempts: the error from the last attempt
118   and a default-constructed endpoint; 118   and a default-constructed endpoint;
119   - on empty range: `std::errc::no_such_device_or_address` and a 119   - on empty range: `std::errc::no_such_device_or_address` and a
120   default-constructed endpoint. 120   default-constructed endpoint.
121   121  
122   @note The socket is closed and re-opened before each attempt, so 122   @note The socket is closed and re-opened before each attempt, so
123   any socket options set by the caller (e.g. `no_delay`, 123   any socket options set by the caller (e.g. `no_delay`,
124   `reuse_address`) are lost. Apply options after this operation 124   `reuse_address`) are lost. Apply options after this operation
125   completes. 125   completes.
126   126  
127   If auto-opening the socket fails during an attempt, that attempt 127   If auto-opening the socket fails during an attempt, that attempt
128   completes with the open error (inherits the contract of 128   completes with the open error (inherits the contract of
129   `Socket::connect`). 129   `Socket::connect`).
130   130  
131   @par Example 131   @par Example
132   @code 132   @code
133   resolver r(ioc); 133   resolver r(ioc);
134   auto [rec, results] = co_await r.resolve("www.boost.org", "80"); 134   auto [rec, results] = co_await r.resolve("www.boost.org", "80");
135   if (rec) co_return; 135   if (rec) co_return;
136   tcp_socket s(ioc); 136   tcp_socket s(ioc);
137   auto [cec, ep] = co_await corosio::connect(s, results); 137   auto [cec, ep] = co_await corosio::connect(s, results);
138   @endcode 138   @endcode
139   */ 139   */
140   template<class Socket, std::ranges::input_range Range> 140   template<class Socket, std::ranges::input_range Range>
141   requires std::convertible_to< 141   requires std::convertible_to<
142   std::ranges::range_reference_t<Range>, 142   std::ranges::range_reference_t<Range>,
143   typename Socket::endpoint_type> 143   typename Socket::endpoint_type>
144   capy::task<capy::io_result<typename Socket::endpoint_type>> 144   capy::task<capy::io_result<typename Socket::endpoint_type>>
HITCBC 145   12 connect(Socket& s, Range endpoints) 145   12 connect(Socket& s, Range endpoints)
146   { 146   {
MISUBC 147   return corosio::connect( 147   return corosio::connect(
HITCBC 148   12 s, std::move(endpoints), detail::default_connect_condition{}); 148   12 s, std::move(endpoints), detail::default_connect_condition{});
149   } 149   }
150   150  
151   /** Asynchronously connect a socket by trying each endpoint in a range, 151   /** Asynchronously connect a socket by trying each endpoint in a range,
152   filtered by a user-supplied condition. 152   filtered by a user-supplied condition.
153   153  
154   For each candidate the condition is invoked as 154   For each candidate the condition is invoked as
155   `cond(last_ec, ep)` where `last_ec` is the error from the most 155   `cond(last_ec, ep)` where `last_ec` is the error from the most
156   recent attempt (default-constructed before the first attempt). If 156   recent attempt (default-constructed before the first attempt). If
157   the condition returns `false` the candidate is skipped; otherwise a 157   the condition returns `false` the candidate is skipped; otherwise a
158   connect is attempted. 158   connect is attempted.
159   159  
160   @param s The socket to connect. See the non-condition overload for 160   @param s The socket to connect. See the non-condition overload for
161   requirements. 161   requirements.
162   @param endpoints A range of candidate endpoints, taken by value. See 162   @param endpoints A range of candidate endpoints, taken by value. See
163   the non-condition overload for the deep-copy caveat when passing 163   the non-condition overload for the deep-copy caveat when passing
164   an lvalue `resolver_results`. 164   an lvalue `resolver_results`.
165   @param cond A predicate invocable with 165   @param cond A predicate invocable with
166   `(std::error_code const&, typename Socket::endpoint_type const&)` 166   `(std::error_code const&, typename Socket::endpoint_type const&)`
167   returning a value contextually convertible to `bool`. 167   returning a value contextually convertible to `bool`.
168   168  
169   @return Same as the non-condition overload. If every candidate is 169   @return Same as the non-condition overload. If every candidate is
170   rejected, completes with `std::errc::no_such_device_or_address`. 170   rejected, completes with `std::errc::no_such_device_or_address`.
171   171  
172   If auto-opening the socket fails, the attempt completes with the 172   If auto-opening the socket fails, the attempt completes with the
173   open error. 173   open error.
174   */ 174   */
175   template<class Socket, std::ranges::input_range Range, class ConnectCondition> 175   template<class Socket, std::ranges::input_range Range, class ConnectCondition>
176   requires std::convertible_to< 176   requires std::convertible_to<
177   std::ranges::range_reference_t<Range>, 177   std::ranges::range_reference_t<Range>,
178   typename Socket::endpoint_type> && 178   typename Socket::endpoint_type> &&
179   std::predicate< 179   std::predicate<
180   ConnectCondition&, 180   ConnectCondition&,
181   std::error_code const&, 181   std::error_code const&,
182   typename Socket::endpoint_type const&> 182   typename Socket::endpoint_type const&>
183   capy::task<capy::io_result<typename Socket::endpoint_type>> 183   capy::task<capy::io_result<typename Socket::endpoint_type>>
HITCBC 184   16 connect(Socket& s, Range endpoints, ConnectCondition cond) 184   16 connect(Socket& s, Range endpoints, ConnectCondition cond)
185   { 185   {
186   using endpoint_type = typename Socket::endpoint_type; 186   using endpoint_type = typename Socket::endpoint_type;
187   187  
188   std::error_code last_ec; 188   std::error_code last_ec;
189   189  
190   for (auto&& e : endpoints) 190   for (auto&& e : endpoints)
191   { 191   {
192   endpoint_type ep = e; 192   endpoint_type ep = e;
193   193  
194   if (!cond(static_cast<std::error_code const&>(last_ec), 194   if (!cond(static_cast<std::error_code const&>(last_ec),
195   static_cast<endpoint_type const&>(ep))) 195   static_cast<endpoint_type const&>(ep)))
196   continue; 196   continue;
197   197  
198   if (s.is_open()) 198   if (s.is_open())
199   s.close(); 199   s.close();
200   200  
201   auto [ec] = co_await s.connect(ep); 201   auto [ec] = co_await s.connect(ep);
202   202  
203   if (!ec) 203   if (!ec)
204   co_return {std::error_code{}, std::move(ep)}; 204   co_return {std::error_code{}, std::move(ep)};
205   205  
206   if (ec == capy::cond::canceled) 206   if (ec == capy::cond::canceled)
207   co_return {ec, endpoint_type{}}; 207   co_return {ec, endpoint_type{}};
208   208  
209   last_ec = ec; 209   last_ec = ec;
210   } 210   }
211   211  
212   if (!last_ec) 212   if (!last_ec)
213   last_ec = std::make_error_code(std::errc::no_such_device_or_address); 213   last_ec = std::make_error_code(std::errc::no_such_device_or_address);
214   214  
215   co_return {last_ec, endpoint_type{}}; 215   co_return {last_ec, endpoint_type{}};
HITCBC 216   32 } 216   32 }
217   217  
218   /** Asynchronously connect a socket by trying each endpoint in an 218   /** Asynchronously connect a socket by trying each endpoint in an
219   iterator range. 219   iterator range.
220   220  
221   Behaves like the range overload, except the return value carries 221   Behaves like the range overload, except the return value carries
222   the iterator to the successfully connected endpoint on success, or 222   the iterator to the successfully connected endpoint on success, or
223   `end` on failure. This mirrors Boost.Asio's iterator-based 223   `end` on failure. This mirrors Boost.Asio's iterator-based
224   `async_connect`. 224   `async_connect`.
225   225  
226   @param s The socket to connect. 226   @param s The socket to connect.
227   @param begin The first candidate. 227   @param begin The first candidate.
228   @param end One past the last candidate. 228   @param end One past the last candidate.
229   229  
230   @return An awaitable completing with `capy::io_result<Iter>`: 230   @return An awaitable completing with `capy::io_result<Iter>`:
231   - on success: default error_code and the iterator of the 231   - on success: default error_code and the iterator of the
232   successful endpoint; 232   successful endpoint;
233   - on failure of all attempts: the error from the last attempt 233   - on failure of all attempts: the error from the last attempt
234   and `end`; 234   and `end`;
235   - on empty range: `std::errc::no_such_device_or_address` and 235   - on empty range: `std::errc::no_such_device_or_address` and
236   `end`. 236   `end`.
237   237  
238   If auto-opening the socket fails, the attempt completes with the 238   If auto-opening the socket fails, the attempt completes with the
239   open error. 239   open error.
240   */ 240   */
241   template<class Socket, std::input_iterator Iter> 241   template<class Socket, std::input_iterator Iter>
242   requires std::convertible_to< 242   requires std::convertible_to<
243   std::iter_reference_t<Iter>, 243   std::iter_reference_t<Iter>,
244   typename Socket::endpoint_type> 244   typename Socket::endpoint_type>
245   capy::task<capy::io_result<Iter>> 245   capy::task<capy::io_result<Iter>>
HITCBC 246   4 connect(Socket& s, Iter begin, Iter end) 246   4 connect(Socket& s, Iter begin, Iter end)
247   { 247   {
248   return corosio::connect( 248   return corosio::connect(
249   s, 249   s,
HITCBC 250   4 std::move(begin), 250   4 std::move(begin),
HITCBC 251   4 std::move(end), 251   4 std::move(end),
HITCBC 252   4 detail::default_connect_condition{}); 252   4 detail::default_connect_condition{});
253   } 253   }
254   254  
255   /** Asynchronously connect a socket by trying each endpoint in an 255   /** Asynchronously connect a socket by trying each endpoint in an
256   iterator range, filtered by a user-supplied condition. 256   iterator range, filtered by a user-supplied condition.
257   257  
258   @param s The socket to connect. 258   @param s The socket to connect.
259   @param begin The first candidate. 259   @param begin The first candidate.
260   @param end One past the last candidate. 260   @param end One past the last candidate.
261   @param cond A predicate invocable with 261   @param cond A predicate invocable with
262   `(std::error_code const&, typename Socket::endpoint_type const&)`. 262   `(std::error_code const&, typename Socket::endpoint_type const&)`.
263   263  
264   @return Same as the plain iterator overload. If every candidate is 264   @return Same as the plain iterator overload. If every candidate is
265   rejected, completes with `std::errc::no_such_device_or_address`. 265   rejected, completes with `std::errc::no_such_device_or_address`.
266   266  
267   If auto-opening the socket fails, the attempt completes with the 267   If auto-opening the socket fails, the attempt completes with the
268   open error. 268   open error.
269   */ 269   */
270   template<class Socket, std::input_iterator Iter, class ConnectCondition> 270   template<class Socket, std::input_iterator Iter, class ConnectCondition>
271   requires std::convertible_to< 271   requires std::convertible_to<
272   std::iter_reference_t<Iter>, 272   std::iter_reference_t<Iter>,
273   typename Socket::endpoint_type> && 273   typename Socket::endpoint_type> &&
274   std::predicate< 274   std::predicate<
275   ConnectCondition&, 275   ConnectCondition&,
276   std::error_code const&, 276   std::error_code const&,
277   typename Socket::endpoint_type const&> 277   typename Socket::endpoint_type const&>
278   capy::task<capy::io_result<Iter>> 278   capy::task<capy::io_result<Iter>>
HITCBC 279   4 connect(Socket& s, Iter begin, Iter end, ConnectCondition cond) 279   4 connect(Socket& s, Iter begin, Iter end, ConnectCondition cond)
280   { 280   {
281   using endpoint_type = typename Socket::endpoint_type; 281   using endpoint_type = typename Socket::endpoint_type;
282   282  
283   std::error_code last_ec; 283   std::error_code last_ec;
284   284  
285   for (Iter it = begin; it != end; ++it) 285   for (Iter it = begin; it != end; ++it)
286   { 286   {
287   endpoint_type ep = *it; 287   endpoint_type ep = *it;
288   288  
289   if (!cond(static_cast<std::error_code const&>(last_ec), 289   if (!cond(static_cast<std::error_code const&>(last_ec),
290   static_cast<endpoint_type const&>(ep))) 290   static_cast<endpoint_type const&>(ep)))
291   continue; 291   continue;
292   292  
293   if (s.is_open()) 293   if (s.is_open())
294   s.close(); 294   s.close();
295   295  
296   auto [ec] = co_await s.connect(ep); 296   auto [ec] = co_await s.connect(ep);
297   297  
298   if (!ec) 298   if (!ec)
299   co_return {std::error_code{}, std::move(it)}; 299   co_return {std::error_code{}, std::move(it)};
300   300  
301   if (ec == capy::cond::canceled) 301   if (ec == capy::cond::canceled)
302   co_return {ec, std::move(end)}; 302   co_return {ec, std::move(end)};
303   303  
304   last_ec = ec; 304   last_ec = ec;
305   } 305   }
306   306  
307   if (!last_ec) 307   if (!last_ec)
308   last_ec = std::make_error_code(std::errc::no_such_device_or_address); 308   last_ec = std::make_error_code(std::errc::no_such_device_or_address);
309   309  
310   co_return {last_ec, std::move(end)}; 310   co_return {last_ec, std::move(end)};
HITCBC 311   8 } 311   8 }
312   312  
313   } // namespace boost::corosio 313   } // namespace boost::corosio
314   314  
315   #endif 315   #endif