Skip to content

Commit 18c30d2

Browse files
committed
Replace void filtering with monostate in when_all to preserve index mapping (#204)
1 parent e2de31c commit 18c30d2

8 files changed

Lines changed: 132 additions & 137 deletions

File tree

doc/modules/ROOT/pages/4.coroutines/4f.composition.adoc

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ task<> example()
5858

5959
`when_all` returns a tuple of results in the same order as the input tasks. Use structured bindings to unpack them.
6060

61-
=== Void Filtering
61+
=== Void Tasks
6262

63-
Tasks returning `void` do not contribute to the result tuple:
63+
Tasks returning `void` contribute `std::monostate` to the result tuple, preserving the task-index-to-result-index mapping:
6464

6565
[source,cpp]
6666
----
@@ -69,18 +69,21 @@ task<int> int_task() { co_return 42; }
6969
7070
task<> example()
7171
{
72-
auto [value] = co_await when_all(void_task(), int_task(), void_task());
73-
// value == 42 (only the int_task contributes)
72+
auto [a, b, c] = co_await when_all(int_task(), void_task(), int_task());
73+
// a == 42 (int, index 0)
74+
// b == monostate (monostate, index 1)
75+
// c == 42 (int, index 2)
7476
}
7577
----
7678

77-
If all tasks return `void`, `when_all` returns `void`:
79+
If all tasks return `void`, `when_all` returns a tuple of `std::monostate`:
7880

7981
[source,cpp]
8082
----
8183
task<> example()
8284
{
83-
co_await when_all(void_task_a(), void_task_b()); // Returns void
85+
auto [a, b] = co_await when_all(void_task_a(), void_task_b());
86+
// a and b are std::monostate
8487
}
8588
----
8689

doc/unlisted/coroutines-when-all.adoc

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ one finishes.
5656

5757
== Return Value
5858

59-
`when_all` returns a tuple of results, with void types filtered out:
59+
`when_all` returns a tuple of results. Void tasks contribute `std::monostate` to preserve the task-index-to-result-index mapping:
6060

6161
[source,cpp]
6262
----
@@ -67,20 +67,20 @@ auto [x, y] = co_await when_all(
6767
);
6868
// x is int, y is std::string
6969
70-
// Mixed with void: void tasks don't contribute
71-
auto [value] = co_await when_all(
72-
task_returning_int(), // task<int>
73-
task_void(), // task<void> - no contribution
74-
task_void() // task<void> - no contribution
70+
// Mixed with void: void tasks contribute monostate
71+
auto [a, b, c] = co_await when_all(
72+
task_returning_int(), // task<int> — index 0
73+
task_void(), // task<void> — index 1 → monostate
74+
task_void() // task<void> — index 2 → monostate
7575
);
76-
// value is int (only non-void result)
76+
// a is int, b and c are std::monostate
7777
78-
// All void: returns void
79-
co_await when_all(
78+
// All void: returns tuple of monostate
79+
auto [m, n] = co_await when_all(
8080
task_void(),
8181
task_void()
8282
);
83-
// No tuple, no return value
83+
// m and n are std::monostate
8484
----
8585

8686
Results appear in the same order as the input tasks.
@@ -255,7 +255,7 @@ Do NOT use `when_all` when:
255255
| Launch tasks concurrently, wait for all
256256

257257
| Return type
258-
| Tuple of non-void results in input order
258+
| Tuple of results in input order (`monostate` for void tasks)
259259

260260
| Error handling
261261
| First exception propagated, siblings get stop

doc/unlisted/library-when-all.adoc

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ one finishes.
5454

5555
== Return Value
5656

57-
`when_all` returns a tuple of results, with void types filtered out:
57+
`when_all` returns a tuple of results. Void tasks contribute `std::monostate` to preserve the task-index-to-result-index mapping:
5858

5959
[source,cpp]
6060
----
@@ -65,20 +65,20 @@ auto [x, y] = co_await when_all(
6565
);
6666
// x is int, y is std::string
6767
68-
// Mixed: void tasks don't contribute
69-
auto [value] = co_await when_all(
70-
returns_int(), // task<int>
71-
returns_void(), // task<void> — no contribution
72-
returns_void() // task<void> — no contribution
68+
// Mixed: void tasks contribute monostate
69+
auto [a, b, c] = co_await when_all(
70+
returns_int(), // task<int> — index 0
71+
returns_void(), // task<void> — index 1 → monostate
72+
returns_void() // task<void> — index 2 → monostate
7373
);
74-
// value is int (only non-void result)
74+
// a is int, b and c are std::monostate
7575
76-
// All void: returns void
77-
co_await when_all(
76+
// All void: returns tuple of monostate
77+
auto [m, n] = co_await when_all(
7878
task_void_1(),
7979
task_void_2()
8080
);
81-
// No tuple, no return value
81+
// m and n are std::monostate
8282
----
8383

8484
Results appear in the same order as input tasks.
@@ -244,7 +244,7 @@ task<void> fetch_all(http_client& client)
244244
| Launch tasks concurrently, wait for all
245245

246246
| Return type
247-
| Tuple of non-void results in input order
247+
| Tuple of results in input order (`monostate` for void tasks)
248248

249249
| Error handling
250250
| First exception propagated, siblings get stop

example/parallel-fetch/parallel_fetch.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,12 @@ capy::task<std::string> fetch_with_side_effects()
8181
{
8282
std::cout << "\n=== Fetch with side effects ===\n";
8383

84-
// void tasks don't contribute to result tuple
85-
std::tuple<std::string> results = co_await capy::when_all(
86-
log_access("api/data"), // void - no result
87-
update_metrics("api_calls"), // void - no result
84+
// void tasks contribute monostate to preserve index mapping
85+
auto [log, metrics, data] = co_await capy::when_all(
86+
log_access("api/data"), // void → monostate
87+
update_metrics("api_calls"), // void → monostate
8888
fetch_user_name(42) // returns string
8989
);
90-
std::string data = std::get<0>(results); // std::string
9190

9291
std::cout << "Data: " << data << "\n";
9392
co_return data;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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/capy
8+
//
9+
10+
#ifndef BOOST_CAPY_DETAIL_VOID_TO_MONOSTATE_HPP
11+
#define BOOST_CAPY_DETAIL_VOID_TO_MONOSTATE_HPP
12+
13+
#include <type_traits>
14+
#include <variant>
15+
16+
namespace boost {
17+
namespace capy {
18+
19+
/** Map void to std::monostate, pass other types through unchanged.
20+
21+
std::variant<void, ...> and std::tuple members of type void are
22+
ill-formed, so void-returning tasks contribute std::monostate
23+
instead. This preserves task-index-to-result-index mapping in
24+
both when_all and when_any.
25+
*/
26+
template<typename T>
27+
using void_to_monostate_t = std::conditional_t<std::is_void_v<T>, std::monostate, T>;
28+
29+
} // namespace capy
30+
} // namespace boost
31+
32+
#endif

include/boost/capy/when_all.hpp

Lines changed: 25 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#define BOOST_CAPY_WHEN_ALL_HPP
1212

1313
#include <boost/capy/detail/config.hpp>
14+
#include <boost/capy/detail/void_to_monostate.hpp>
1415
#include <boost/capy/concept/executor.hpp>
1516
#include <boost/capy/concept/io_awaitable.hpp>
1617
#include <coroutine>
@@ -32,19 +33,6 @@ namespace capy {
3233

3334
namespace detail {
3435

35-
/** Type trait to filter void types from a tuple.
36-
37-
Void-returning tasks do not contribute a value to the result tuple.
38-
This trait computes the filtered result type.
39-
40-
Example: filter_void_tuple_t<int, void, string> = tuple<int, string>
41-
*/
42-
template<typename T>
43-
using wrap_non_void_t = std::conditional_t<std::is_void_v<T>, std::tuple<>, std::tuple<T>>;
44-
45-
template<typename... Ts>
46-
using filter_void_tuple_t = decltype(std::tuple_cat(std::declval<wrap_non_void_t<Ts>>()...));
47-
4836
/** Holds the result of a single task within when_all.
4937
*/
5038
template<typename T>
@@ -63,11 +51,12 @@ struct result_holder
6351
}
6452
};
6553

66-
/** Specialization for void tasks - no value storage needed.
54+
/** Specialization for void tasks - returns monostate to preserve index mapping.
6755
*/
6856
template<>
6957
struct result_holder<void>
7058
{
59+
std::monostate get() && { return {}; }
7160
};
7261

7362
/** Shared state for when_all operation.
@@ -358,45 +347,38 @@ class when_all_launcher
358347
}
359348
};
360349

361-
/** Helper to extract a single result, returning empty tuple for void.
350+
/** Helper to extract a single result from state.
362351
This is a separate function to work around a GCC-11 ICE that occurs
363352
when using nested immediately-invoked lambdas with pack expansion.
364353
*/
365354
template<std::size_t I, typename... Ts>
366355
auto extract_single_result(when_all_state<Ts...>& state)
367356
{
368-
using T = std::tuple_element_t<I, std::tuple<Ts...>>;
369-
if constexpr (std::is_void_v<T>)
370-
return std::tuple<>();
371-
else
372-
return std::make_tuple(std::move(std::get<I>(state.results_)).get());
357+
return std::move(std::get<I>(state.results_)).get();
373358
}
374359

375-
/** Extract results from state, filtering void types.
360+
/** Extract all results from state as a tuple.
376361
*/
377362
template<typename... Ts>
378363
auto extract_results(when_all_state<Ts...>& state)
379364
{
380365
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
381-
return std::tuple_cat(extract_single_result<Is>(state)...);
366+
return std::tuple(extract_single_result<Is>(state)...);
382367
}(std::index_sequence_for<Ts...>{});
383368
}
384369

385370
} // namespace detail
386371

387-
/** Compute a tuple type with void types filtered out.
372+
/** Compute the when_all result tuple type.
388373
389-
Returns void when all types are void (P2300 aligned),
390-
otherwise returns a std::tuple with void types removed.
374+
Void-returning tasks contribute std::monostate to preserve the
375+
task-index-to-result-index mapping, matching when_any's approach.
391376
392-
Example: non_void_tuple_t<int, void, string> = std::tuple<int, string>
393-
Example: non_void_tuple_t<void, void> = void
377+
Example: when_all_result_t<int, void, string> = std::tuple<int, std::monostate, string>
378+
Example: when_all_result_t<void, void> = std::tuple<std::monostate, std::monostate>
394379
*/
395380
template<typename... Ts>
396-
using non_void_tuple_t = std::conditional_t<
397-
std::is_same_v<detail::filter_void_tuple_t<Ts...>, std::tuple<>>,
398-
void,
399-
detail::filter_void_tuple_t<Ts...>>;
381+
using when_all_result_t = std::tuple<void_to_monostate_t<Ts>...>;
400382

401383
/** Execute multiple awaitables concurrently and collect their results.
402384
@@ -407,8 +389,8 @@ using non_void_tuple_t = std::conditional_t<
407389
408390
@li All child awaitables run concurrently on the caller's executor
409391
@li Results are returned as a tuple in input order
410-
@li Void-returning awaitables do not contribute to the result tuple
411-
@li If all awaitables return void, `when_all` returns `task<void>`
392+
@li Void-returning awaitables contribute std::monostate to the
393+
result tuple, preserving the task-index-to-result-index mapping
412394
@li First exception wins; subsequent exceptions are discarded
413395
@li Stop is requested for siblings on first error
414396
@li Completes only after all children have finished
@@ -422,8 +404,8 @@ using non_void_tuple_t = std::conditional_t<
422404
satisfy @ref IoAwaitable and is consumed (moved-from) when
423405
`when_all` is awaited.
424406
425-
@return A task yielding a tuple of non-void results. Returns
426-
`task<void>` when all input awaitables return void.
407+
@return A task yielding a tuple of results in input order. Void tasks
408+
contribute std::monostate to preserve index correspondence.
427409
428410
@par Example
429411
@@ -436,23 +418,22 @@ using non_void_tuple_t = std::conditional_t<
436418
fetch_posts( id ) // task<std::vector<Post>>
437419
);
438420
439-
// Void awaitables don't contribute to result
440-
co_await when_all(
441-
log_event( "start" ), // task<void>
442-
notify_user( id ) // task<void>
421+
// Void awaitables contribute monostate
422+
auto [a, _, b] = co_await when_all(
423+
fetch_int(), // task<int>
424+
log_event( "start" ), // task<void> → monostate
425+
fetch_str() // task<string>
443426
);
444-
// Returns task<void>, no result tuple
427+
// a is int, _ is monostate, b is string
445428
}
446429
@endcode
447430
448431
@see IoAwaitable, task
449432
*/
450433
template<IoAwaitable... As>
451434
[[nodiscard]] auto when_all(As... awaitables)
452-
-> task<non_void_tuple_t<awaitable_result_t<As>...>>
435+
-> task<when_all_result_t<awaitable_result_t<As>...>>
453436
{
454-
using result_type = non_void_tuple_t<awaitable_result_t<As>...>;
455-
456437
// State is stored in the coroutine frame, using the frame allocator
457438
detail::when_all_state<awaitable_result_t<As>...> state;
458439

@@ -469,11 +450,7 @@ template<IoAwaitable... As>
469450
if(state.first_exception_)
470451
std::rethrow_exception(state.first_exception_);
471452

472-
// Extract and return results
473-
if constexpr (std::is_void_v<result_type>)
474-
co_return;
475-
else
476-
co_return detail::extract_results(state);
453+
co_return detail::extract_results(state);
477454
}
478455

479456
} // namespace capy

include/boost/capy/when_any.hpp

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#define BOOST_CAPY_WHEN_ANY_HPP
1212

1313
#include <boost/capy/detail/config.hpp>
14+
#include <boost/capy/detail/void_to_monostate.hpp>
1415
#include <boost/capy/concept/executor.hpp>
1516
#include <boost/capy/concept/io_awaitable.hpp>
1617
#include <coroutine>
@@ -115,17 +116,6 @@
115116
namespace boost {
116117
namespace capy {
117118

118-
/** Convert void to monostate for variant storage.
119-
120-
std::variant<void, ...> is ill-formed, so void tasks contribute
121-
std::monostate to the result variant instead. Non-void types
122-
pass through unchanged.
123-
124-
@tparam T The type to potentially convert (void becomes monostate).
125-
*/
126-
template<typename T>
127-
using void_to_monostate_t = std::conditional_t<std::is_void_v<T>, std::monostate, T>;
128-
129119
namespace detail {
130120

131121
/** Core shared state for when_any operations.

0 commit comments

Comments
 (0)