Skip to content

Commit ddca322

Browse files
committed
⚡ Add gather_by for tuples
This replaces `sort | chunk_by` as used by CIB. This improves the CIB compilation benchmark: `gather_by` is roughly 1.4x faster than `sort | chunk_by`.
1 parent a5fd19f commit ddca322

File tree

3 files changed

+180
-21
lines changed

3 files changed

+180
-21
lines changed

docs/tuple_algorithms.adoc

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,28 @@ auto a = std::array{1, 2, 3};
176176
stdx::unrolled_for_each([] (auto x) { std::cout << x << '\n'; }, a);
177177
----
178178

179+
=== `gather_by`
180+
181+
`gather_by` takes a tuple and returns a tuple-of-tuples, where each tuple is
182+
grouped by type name.
183+
[source,cpp]
184+
----
185+
auto t = stdx::tuple{1, true, 2, false, 3}; // tuple<int, int, int, bool, bool>
186+
auto c1 = stdx::gather_by(t); // tuple<tuple<int, int, int>, tuple<bool, bool>>
187+
auto c2 = stdx::gather(t); // without a template argument, the same as gather_by
188+
----
189+
190+
`gather_by` is like `chunk_by`, except that `gather_by` gathers elements that are not adjacent.
191+
192+
`gather_by` takes an optional template argument which is a type
193+
function (a template of one argument). This will be applied to each type in the
194+
tuple to obtain a type name that is then used to chunk. By default, this
195+
type function is `std::type_identity_t`.
196+
197+
WARNING: `gather_by` uses `sort` - not `stable_sort`! For a given type, the
198+
order of values in the gathered tuple is not necessarily the same as that of the
199+
input tuple.
200+
179201
=== `sort`
180202

181203
`sort` is used to sort a tuple by type name.
@@ -190,6 +212,9 @@ function (a template of one argument). This will be applied to each type in the
190212
tuple to obtain a type name that is then sorted alphabetically. By default, this
191213
type function is `std::type_identity_t`.
192214

215+
WARNING: `sort` is not `stable_sort`! For a given type, the order of values in
216+
the sorted tuple is not necessarily the same as that of the input tuple.
217+
193218
=== `to_sorted_set`
194219

195220
`to_sorted_set` is `sort` followed by `unique`: it sorts the types in a tuple,
@@ -198,9 +223,13 @@ then collapses it so that there is only one element of each type.
198223
[source,cpp]
199224
----
200225
auto t = stdx::tuple{1, true, 2, false};
201-
auto u = stdx::to_sorted_set(t); // {true, 1}
226+
auto u = stdx::to_sorted_set(t); // {<some bool>, <some integer>}
202227
----
203228

229+
WARNING: `sort` is not `stable_sort`! The value in the example above is not
230+
necessarily `{true, 1}` because there is no stable ordering between elements of
231+
the same type.
232+
204233
=== `to_unsorted_set`
205234

206235
`to_unsorted_set` produces a tuple of unique types in the same order as the

include/stdx/tuple_algorithms.hpp

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,21 @@ constexpr auto contains_type(
173173
stdx::detail::tuple_impl<IndexSeq, index_function_list<Fs...>, Us...> const
174174
&) -> std::bool_constant<(is_index_for<T, Fs, Us...> or ...) or
175175
(std::is_same_v<T, Us> or ...)>;
176+
177+
template <tuplelike T, template <typename> typename Proj = std::type_identity_t>
178+
[[nodiscard]] constexpr auto sorted_indices() {
179+
return []<std::size_t... Is>(std::index_sequence<Is...>)
180+
-> std::array<std::size_t, sizeof...(Is)> {
181+
using P = std::pair<std::string_view, std::size_t>;
182+
auto a = std::array<P, sizeof...(Is)>{
183+
P{stdx::type_as_string<Proj<tuple_element_t<Is, T>>>(), Is}...};
184+
std::sort(a.begin(), a.end(), [](auto const &p1, auto const &p2) {
185+
return p1.first < p2.first;
186+
});
187+
return {a[Is].second...};
188+
}
189+
(std::make_index_sequence<T::size()>{});
190+
}
176191
} // namespace detail
177192

178193
template <tuplelike Tuple, typename T>
@@ -183,36 +198,29 @@ template <template <typename> typename Proj = std::type_identity_t,
183198
tuplelike Tuple>
184199
[[nodiscard]] constexpr auto sort(Tuple &&t) {
185200
using T = stdx::remove_cvref_t<Tuple>;
186-
using P = std::pair<std::string_view, std::size_t>;
187-
constexpr auto indices = []<std::size_t... Is>(std::index_sequence<Is...>) {
188-
auto a = std::array<P, sizeof...(Is)>{
189-
P{stdx::type_as_string<Proj<tuple_element_t<Is, T>>>(), Is}...};
190-
std::sort(a.begin(), a.end(), [](auto const &p1, auto const &p2) {
191-
return p1.first < p2.first;
192-
});
193-
return a;
194-
}(std::make_index_sequence<T::size()>{});
195-
201+
constexpr auto indices = detail::sorted_indices<T, Proj>();
196202
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
197-
return stdx::tuple<tuple_element_t<indices[Is].second, T>...>{
198-
std::forward<Tuple>(t)[index<indices[Is].second>]...};
203+
return stdx::tuple<tuple_element_t<indices[Is], T>...>{
204+
std::forward<Tuple>(t)[index<indices[Is]>]...};
199205
}(std::make_index_sequence<T::size()>{});
200206
}
201207

202208
namespace detail {
203-
template <tuplelike T, template <typename> typename Proj, std::size_t I>
204-
[[nodiscard]] constexpr auto test_adjacent() -> bool {
205-
return std::is_same_v<Proj<stdx::tuple_element_t<I, T>>,
206-
Proj<stdx::tuple_element_t<I + 1, T>>>;
207-
}
209+
210+
template <tuplelike T, template <typename> typename Proj> struct test_pair_t {
211+
template <std::size_t I, std::size_t J>
212+
constexpr static auto value =
213+
std::is_same_v<Proj<stdx::tuple_element_t<I, T>>,
214+
Proj<stdx::tuple_element_t<J, T>>>;
215+
};
208216

209217
template <tuplelike T, template <typename> typename Proj = std::type_identity_t>
210218
requires(tuple_size_v<T> > 1)
211219
[[nodiscard]] constexpr auto count_chunks() {
212220
auto count = std::size_t{1};
213221
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
214-
((count +=
215-
static_cast<std::size_t>(not detail::test_adjacent<T, Proj, Is>())),
222+
((count += static_cast<std::size_t>(
223+
not test_pair_t<T, Proj>::template value<Is, Is + 1>)),
216224
...);
217225
}(std::make_index_sequence<stdx::tuple_size_v<T> - 1>{});
218226
return count;
@@ -232,7 +240,7 @@ template <tuplelike T, template <typename> typename Proj = std::type_identity_t>
232240
std::array<chunk, count_chunks<T, Proj>()> chunks{};
233241
++chunks[index].size;
234242
auto check_next_chunk = [&]<std::size_t I>() {
235-
if (not detail::test_adjacent<T, Proj, I>()) {
243+
if (not test_pair_t<T, Proj>::template value<I, I + 1>) {
236244
chunks[++index].offset = I + 1;
237245
}
238246
++chunks[index].size;
@@ -333,6 +341,55 @@ template <tuplelike Tuple> constexpr auto to_unsorted_set(Tuple &&t) {
333341
std::forward<Tuple>(t))...};
334342
}(std::make_index_sequence<U::size()>{});
335343
}
344+
345+
template <template <typename> typename Proj = std::type_identity_t,
346+
tuplelike Tuple>
347+
[[nodiscard]] constexpr auto gather_by(Tuple &&t) {
348+
using tuple_t = std::remove_cvref_t<Tuple>;
349+
if constexpr (tuple_size_v<tuple_t> == 0) {
350+
return stdx::tuple{};
351+
} else if constexpr (tuple_size_v<tuple_t> == 1) {
352+
return stdx::make_tuple(std::forward<Tuple>(t));
353+
} else {
354+
constexpr auto sorted_idxs = detail::sorted_indices<tuple_t, Proj>();
355+
constexpr auto tests =
356+
[&]<std::size_t... Is>(std::index_sequence<Is...>) {
357+
return std::array<bool, stdx::tuple_size_v<tuple_t> - 1>{
358+
detail::test_pair_t<tuple_t, Proj>::template value<
359+
sorted_idxs[Is], sorted_idxs[Is + 1]>...};
360+
}(std::make_index_sequence<stdx::tuple_size_v<tuple_t> - 1>{});
361+
362+
constexpr auto chunks = [&] {
363+
constexpr auto chunk_count =
364+
std::count(std::begin(tests), std::end(tests), false) + 1;
365+
std::array<detail::chunk, chunk_count> cs{};
366+
367+
auto index = std::size_t{};
368+
++cs[index].size;
369+
for (auto i = std::size_t{}; i < std::size(tests); ++i) {
370+
if (not tests[i]) {
371+
cs[++index].offset = i + 1;
372+
}
373+
++cs[index].size;
374+
}
375+
return cs;
376+
}();
377+
378+
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
379+
return stdx::make_tuple([&]<std::size_t... Js>(
380+
std::index_sequence<Js...>) {
381+
constexpr auto offset = chunks[Is].offset;
382+
return stdx::tuple<
383+
tuple_element_t<sorted_idxs[offset + Js], tuple_t>...>{
384+
std::forward<Tuple>(t)[index<sorted_idxs[offset + Js]>]...};
385+
}(std::make_index_sequence<chunks[Is].size>{})...);
386+
}(std::make_index_sequence<chunks.size()>{});
387+
}
388+
}
389+
390+
template <tuplelike Tuple> [[nodiscard]] constexpr auto gather(Tuple &&t) {
391+
return gather_by(std::forward<Tuple>(t));
392+
}
336393
} // namespace v1
337394
} // namespace stdx
338395

test/tuple_algorithms.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,76 @@ TEST_CASE("unrolled enumerate on arrays", "[tuple_algorithms]") {
649649
a, a);
650650
CHECK(sum == (0 + 1 + 2) + (2 + 4 + 6));
651651
}
652+
653+
TEST_CASE("gather (empty tuple)", "[tuple_algorithms]") {
654+
constexpr auto t = stdx::tuple{};
655+
[[maybe_unused]] constexpr auto gathered = stdx::gather(t);
656+
static_assert(std::is_same_v<decltype(gathered), stdx::tuple<> const>);
657+
}
658+
659+
TEST_CASE("gather (1-element tuple)", "[tuple_algorithms]") {
660+
constexpr auto t = stdx::tuple{1};
661+
constexpr auto gathered = stdx::gather(t);
662+
static_assert(std::is_same_v<decltype(gathered),
663+
stdx::tuple<stdx::tuple<int>> const>);
664+
CHECK(gathered == stdx::make_tuple(stdx::tuple{1}));
665+
}
666+
667+
TEST_CASE("gather (general case)", "[tuple_algorithms]") {
668+
constexpr auto t = stdx::tuple{1, 1.0, 2, 1.0, 3, true};
669+
constexpr auto gathered = stdx::gather(t);
670+
static_assert(std::is_same_v<
671+
decltype(gathered),
672+
stdx::tuple<stdx::tuple<bool>, stdx::tuple<double, double>,
673+
stdx::tuple<int, int, int>> const>);
674+
// NB: the subtuples are not necessarily ordered the same way as originally
675+
CHECK(stdx::get<0>(gathered) == stdx::tuple{true});
676+
CHECK(stdx::get<1>(gathered).fold_left(0.0, std::plus{}) == 2.0);
677+
CHECK(stdx::get<2>(gathered).fold_left(0, std::plus{}) == 6);
678+
}
679+
680+
TEST_CASE("gather preserves references", "[tuple_algorithms]") {
681+
int x{1};
682+
int y{2};
683+
auto t = stdx::tuple<int &, int &>{x, y};
684+
auto gathered = stdx::gather(t);
685+
static_assert(std::is_same_v<decltype(gathered),
686+
stdx::tuple<stdx::tuple<int &, int &>>>);
687+
CHECK(get<0>(gathered) == stdx::tuple{1, 2});
688+
}
689+
690+
TEST_CASE("gather with move only types", "[tuple_algorithms]") {
691+
auto t = stdx::tuple{move_only{1}};
692+
auto gathered = stdx::gather(std::move(t));
693+
static_assert(std::is_same_v<decltype(gathered),
694+
stdx::tuple<stdx::tuple<move_only>>>);
695+
CHECK(get<0>(gathered) == stdx::tuple{move_only{1}});
696+
}
697+
698+
namespace {
699+
template <typename T> struct named_int {
700+
using name_t = T;
701+
int value;
702+
friend constexpr auto operator==(named_int, named_int) -> bool = default;
703+
};
704+
705+
template <typename T> using name_of_t = typename T::name_t;
706+
} // namespace
707+
708+
TEST_CASE("gather_by with projection", "[tuple_algorithms]") {
709+
struct A;
710+
struct B;
711+
struct C;
712+
constexpr auto t = stdx::tuple{named_int<C>{3}, named_int<B>{11},
713+
named_int<A>{0}, named_int<B>{12}};
714+
constexpr auto gathered = stdx::gather_by<name_of_t>(t);
715+
static_assert(
716+
std::is_same_v<decltype(gathered),
717+
stdx::tuple<stdx::tuple<named_int<A>>,
718+
stdx::tuple<named_int<B>, named_int<B>>,
719+
stdx::tuple<named_int<C>>> const>);
720+
CHECK(get<0>(gathered) == stdx::tuple{named_int<A>{0}});
721+
CHECK(stdx::get<1>(gathered).fold_left(
722+
0, [](auto x, auto y) { return x + y.value; }) == 23);
723+
CHECK(get<2>(gathered) == stdx::tuple{named_int<C>{3}});
724+
}

0 commit comments

Comments
 (0)