Skip to content

Commit f91eb8d

Browse files
[parallel] Add projection support to hpx::is_sorted, hpx::is_sorted_until, and hpx::is_partitioned CPOs
The hpx:: (non-ranges) CPOs for is_sorted, is_sorted_until, and is_partitioned did not expose a Proj parameter in their tag_fallback_invoke overloads, despite the internal algorithm implementations (hpx::parallel::detail::is_sorted, is_sorted_until, is_partitioned) fully supporting projections via the Proj template parameter. This adds new tag_fallback_invoke overloads with Proj defaulting to hpx::identity alongside the existing overloads (which are unchanged for full backward compatibility). The constraint uses hpx::parallel::traits::is_projected_v<Proj, FwdIter> to maintain proper SFINAE behavior. Also adds hpx/algorithms/traits/projected.hpp include to both headers and a new regression test is_sorted_projection.cpp that exercises seq, par, and par(task) policies with all three algorithms. Closes: projection gap between hpx:: and hpx::ranges:: algorithm APIs
1 parent 3546119 commit f91eb8d

4 files changed

Lines changed: 299 additions & 11 deletions

File tree

libs/core/algorithms/include/hpx/parallel/algorithms/is_partitioned.hpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ namespace hpx {
111111
#else
112112

113113
#include <hpx/config.hpp>
114+
#include <hpx/algorithms/traits/projected.hpp>
114115
#include <hpx/modules/executors.hpp>
115116
#include <hpx/modules/functional.hpp>
116117
#include <hpx/modules/iterator_support.hpp>
@@ -120,6 +121,7 @@ namespace hpx {
120121
#include <hpx/parallel/util/detail/algorithm_result.hpp>
121122
#include <hpx/parallel/util/detail/sender_util.hpp>
122123
#include <hpx/parallel/util/invoke_projected.hpp>
124+
123125
#include <hpx/parallel/util/loop.hpp>
124126
#include <hpx/parallel/util/partitioner.hpp>
125127

@@ -283,6 +285,40 @@ namespace hpx {
283285
.call(HPX_FORWARD(ExPolicy, policy), first, last,
284286
HPX_MOVE(pred), hpx::identity_v);
285287
}
288+
289+
template <typename FwdIter, typename Pred,
290+
typename Proj = hpx::identity>
291+
// clang-format off
292+
requires (
293+
std::forward_iterator<FwdIter> &&
294+
hpx::parallel::traits::is_projected_v<Proj, FwdIter>
295+
)
296+
// clang-format on
297+
friend bool tag_fallback_invoke(hpx::is_partitioned_t, FwdIter first,
298+
FwdIter last, Pred pred, Proj proj)
299+
{
300+
return hpx::parallel::detail::is_partitioned<FwdIter, FwdIter>()
301+
.call(hpx::execution::seq, first, last, HPX_MOVE(pred),
302+
HPX_MOVE(proj));
303+
}
304+
305+
template <typename ExPolicy, typename FwdIter, typename Pred,
306+
typename Proj = hpx::identity>
307+
// clang-format off
308+
requires (
309+
hpx::is_execution_policy_v<ExPolicy> &&
310+
std::forward_iterator<FwdIter> &&
311+
hpx::parallel::traits::is_projected_v<Proj, FwdIter>
312+
)
313+
// clang-format on
314+
friend decltype(auto) tag_fallback_invoke(hpx::is_partitioned_t,
315+
ExPolicy&& policy, FwdIter first, FwdIter last, Pred pred,
316+
Proj proj)
317+
{
318+
return hpx::parallel::detail::is_partitioned<FwdIter, FwdIter>()
319+
.call(HPX_FORWARD(ExPolicy, policy), first, last,
320+
HPX_MOVE(pred), HPX_MOVE(proj));
321+
}
286322
} is_partitioned{};
287323
} // namespace hpx
288324

libs/core/algorithms/include/hpx/parallel/algorithms/is_sorted.hpp

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ namespace hpx {
226226
#else
227227

228228
#include <hpx/config.hpp>
229+
#include <hpx/algorithms/traits/projected.hpp>
229230
#include <hpx/modules/coroutines.hpp>
230231
#include <hpx/modules/executors.hpp>
231232
#include <hpx/modules/functional.hpp>
@@ -238,6 +239,7 @@ namespace hpx {
238239
#include <hpx/parallel/util/detail/clear_container.hpp>
239240
#include <hpx/parallel/util/detail/sender_util.hpp>
240241
#include <hpx/parallel/util/invoke_projected.hpp>
242+
241243
#include <hpx/parallel/util/loop.hpp>
242244
#include <hpx/parallel/util/partitioner.hpp>
243245

@@ -459,42 +461,51 @@ namespace hpx {
459461
: hpx::detail::tag_parallel_algorithm<is_sorted_t>
460462
{
461463
private:
462-
template <typename FwdIter, typename Pred = hpx::parallel::detail::less>
464+
template <typename FwdIter, typename Pred = hpx::parallel::detail::less,
465+
typename Proj = hpx::identity>
463466
// clang-format off
464467
requires (
465468
std::forward_iterator<FwdIter> &&
469+
hpx::parallel::traits::is_projected_v<Proj, FwdIter> &&
466470
hpx::is_invocable_v<Pred,
467-
typename std::iterator_traits<FwdIter>::value_type,
468-
typename std::iterator_traits<FwdIter>::value_type
471+
hpx::util::invoke_result_t<Proj,
472+
typename std::iterator_traits<FwdIter>::value_type>,
473+
hpx::util::invoke_result_t<Proj,
474+
typename std::iterator_traits<FwdIter>::value_type>
469475
>
470476
)
471477
// clang-format on
472-
friend bool tag_fallback_invoke(
473-
hpx::is_sorted_t, FwdIter first, FwdIter last, Pred pred = Pred())
478+
friend bool tag_fallback_invoke(hpx::is_sorted_t, FwdIter first,
479+
FwdIter last, Pred pred = Pred(), Proj proj = Proj())
474480
{
475481
return hpx::parallel::detail::is_sorted<FwdIter, FwdIter>().call(
476482
hpx::execution::seq, first, last, HPX_MOVE(pred),
477-
hpx::identity_v);
483+
HPX_MOVE(proj));
478484
}
479485

480486
template <typename ExPolicy, typename FwdIter,
481-
typename Pred = hpx::parallel::detail::less>
487+
typename Pred = hpx::parallel::detail::less,
488+
typename Proj = hpx::identity>
482489
// clang-format off
483490
requires (
484491
hpx::is_execution_policy_v<ExPolicy> &&
485492
std::forward_iterator<FwdIter> &&
493+
hpx::parallel::traits::is_projected_v<Proj, FwdIter> &&
486494
hpx::is_invocable_v<Pred,
487-
typename std::iterator_traits<FwdIter>::value_type,
488-
typename std::iterator_traits<FwdIter>::value_type
495+
hpx::util::invoke_result_t<Proj,
496+
typename std::iterator_traits<FwdIter>::value_type>,
497+
hpx::util::invoke_result_t<Proj,
498+
typename std::iterator_traits<FwdIter>::value_type>
489499
>
490500
)
491501
// clang-format on
492502
friend decltype(auto) tag_fallback_invoke(hpx::is_sorted_t,
493-
ExPolicy&& policy, FwdIter first, FwdIter last, Pred pred = Pred())
503+
ExPolicy&& policy, FwdIter first, FwdIter last, Pred pred = Pred(),
504+
Proj proj = Proj())
494505
{
495506
return hpx::parallel::detail::is_sorted<FwdIter, FwdIter>().call(
496507
HPX_FORWARD(ExPolicy, policy), first, last, HPX_MOVE(pred),
497-
hpx::identity_v);
508+
HPX_MOVE(proj));
498509
}
499510
} is_sorted{};
500511

@@ -539,6 +550,53 @@ namespace hpx {
539550
.call(HPX_FORWARD(ExPolicy, policy), first, last,
540551
HPX_MOVE(pred), hpx::identity_v);
541552
}
553+
554+
template <typename FwdIter, typename Pred = hpx::parallel::detail::less,
555+
typename Proj = hpx::identity>
556+
// clang-format off
557+
requires (
558+
std::forward_iterator<FwdIter> &&
559+
hpx::parallel::traits::is_projected_v<Proj, FwdIter> &&
560+
hpx::is_invocable_v<Pred,
561+
hpx::util::invoke_result_t<Proj,
562+
typename std::iterator_traits<FwdIter>::value_type>,
563+
hpx::util::invoke_result_t<Proj,
564+
typename std::iterator_traits<FwdIter>::value_type>
565+
>
566+
)
567+
// clang-format on
568+
friend FwdIter tag_fallback_invoke(hpx::is_sorted_until_t,
569+
FwdIter first, FwdIter last, Pred pred = Pred(), Proj proj = Proj())
570+
{
571+
return hpx::parallel::detail::is_sorted_until<FwdIter, FwdIter>()
572+
.call(hpx::execution::seq, first, last, HPX_MOVE(pred),
573+
HPX_MOVE(proj));
574+
}
575+
576+
template <typename ExPolicy, typename FwdIter,
577+
typename Pred = hpx::parallel::detail::less,
578+
typename Proj = hpx::identity>
579+
// clang-format off
580+
requires (
581+
hpx::is_execution_policy_v<ExPolicy> &&
582+
std::forward_iterator<FwdIter> &&
583+
hpx::parallel::traits::is_projected_v<Proj, FwdIter> &&
584+
hpx::is_invocable_v<Pred,
585+
hpx::util::invoke_result_t<Proj,
586+
typename std::iterator_traits<FwdIter>::value_type>,
587+
hpx::util::invoke_result_t<Proj,
588+
typename std::iterator_traits<FwdIter>::value_type>
589+
>
590+
)
591+
// clang-format on
592+
friend decltype(auto) tag_fallback_invoke(hpx::is_sorted_until_t,
593+
ExPolicy&& policy, FwdIter first, FwdIter last, Pred pred = Pred(),
594+
Proj proj = Proj())
595+
{
596+
return hpx::parallel::detail::is_sorted_until<FwdIter, FwdIter>()
597+
.call(HPX_FORWARD(ExPolicy, policy), first, last,
598+
HPX_MOVE(pred), HPX_MOVE(proj));
599+
}
542600
} is_sorted_until{};
543601
} // namespace hpx
544602

libs/core/algorithms/tests/unit/algorithms/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ set(tests
7979
inplace_merge
8080
is_partitioned
8181
is_sorted
82+
is_sorted_projection
8283
is_sorted_until
8384
lexicographical_compare
8485
make_heap
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Copyright (c) 2024-2026 STE||AR-Group
2+
//
3+
// SPDX-License-Identifier: BSL-1.0
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+
// Regression test: hpx::is_sorted, hpx::is_sorted_until, and
8+
// hpx::is_partitioned did not expose a projection parameter in their CPO
9+
// (tag_fallback_invoke) overloads, despite the internal algorithm
10+
// implementations fully supporting projections. This file validates that
11+
// all three algorithms accept a projection with seq and par policies.
12+
13+
#include <hpx/algorithm.hpp>
14+
#include <hpx/init.hpp>
15+
#include <hpx/modules/testing.hpp>
16+
17+
#include <cstddef>
18+
#include <functional>
19+
#include <utility>
20+
#include <vector>
21+
22+
// ---------------------------------------------------------------------------
23+
// is_sorted with projection
24+
// ---------------------------------------------------------------------------
25+
void test_is_sorted_projection()
26+
{
27+
using namespace hpx::execution;
28+
using element = std::pair<int, int>;
29+
30+
// sorted by .second: 1, 3, 5, 8
31+
std::vector<element> sorted_c = {{10, 1}, {20, 3}, {30, 5}, {40, 8}};
32+
// NOT sorted by .second: 1, 5, 3, 8
33+
std::vector<element> unsorted_c = {{10, 1}, {20, 5}, {30, 3}, {40, 8}};
34+
35+
auto proj = [](element const& p) { return p.second; };
36+
37+
// sequential, no policy
38+
HPX_TEST(hpx::is_sorted(
39+
sorted_c.begin(), sorted_c.end(), std::less<int>{}, proj));
40+
HPX_TEST(!hpx::is_sorted(
41+
unsorted_c.begin(), unsorted_c.end(), std::less<int>{}, proj));
42+
43+
// seq policy
44+
HPX_TEST(hpx::is_sorted(
45+
seq, sorted_c.begin(), sorted_c.end(), std::less<int>{}, proj));
46+
HPX_TEST(!hpx::is_sorted(
47+
seq, unsorted_c.begin(), unsorted_c.end(), std::less<int>{}, proj));
48+
49+
// par policy
50+
HPX_TEST(hpx::is_sorted(
51+
par, sorted_c.begin(), sorted_c.end(), std::less<int>{}, proj));
52+
HPX_TEST(!hpx::is_sorted(
53+
par, unsorted_c.begin(), unsorted_c.end(), std::less<int>{}, proj));
54+
55+
// par(task) policy
56+
HPX_TEST(hpx::is_sorted(
57+
par(task), sorted_c.begin(), sorted_c.end(), std::less<int>{}, proj)
58+
.get());
59+
HPX_TEST(!hpx::is_sorted(
60+
par(task), unsorted_c.begin(), unsorted_c.end(), std::less<int>{}, proj)
61+
.get());
62+
63+
// empty range always returns true
64+
HPX_TEST(hpx::is_sorted(
65+
par, sorted_c.begin(), sorted_c.begin(), std::less<int>{}, proj));
66+
// single element range always returns true
67+
HPX_TEST(hpx::is_sorted(
68+
par, sorted_c.begin(), sorted_c.begin() + 1, std::less<int>{}, proj));
69+
}
70+
71+
// ---------------------------------------------------------------------------
72+
// is_sorted_until with projection
73+
// ---------------------------------------------------------------------------
74+
void test_is_sorted_until_projection()
75+
{
76+
using namespace hpx::execution;
77+
using element = std::pair<int, int>;
78+
79+
// sorted by .second: 1, 3, 8; then unsorted at index 3 (value 5)
80+
std::vector<element> c = {{10, 1}, {20, 3}, {30, 8}, {40, 5}, {50, 9}};
81+
82+
auto proj = [](element const& p) { return p.second; };
83+
84+
// sequential, no policy
85+
auto it_seq =
86+
hpx::is_sorted_until(c.begin(), c.end(), std::less<int>{}, proj);
87+
HPX_TEST(it_seq != c.end());
88+
HPX_TEST_EQ(it_seq->second, 5);
89+
90+
// seq policy
91+
auto it_seq2 =
92+
hpx::is_sorted_until(seq, c.begin(), c.end(), std::less<int>{}, proj);
93+
HPX_TEST(it_seq2 != c.end());
94+
HPX_TEST_EQ(it_seq2->second, 5);
95+
96+
// par policy
97+
auto it_par =
98+
hpx::is_sorted_until(par, c.begin(), c.end(), std::less<int>{}, proj);
99+
HPX_TEST(it_par != c.end());
100+
HPX_TEST_EQ(it_par->second, 5);
101+
102+
// par(task) policy
103+
auto it_task = hpx::is_sorted_until(
104+
par(task), c.begin(), c.end(), std::less<int>{}, proj)
105+
.get();
106+
HPX_TEST(it_task != c.end());
107+
HPX_TEST_EQ(it_task->second, 5);
108+
109+
// fully sorted range should return end
110+
std::vector<element> fully_sorted = {{10, 1}, {20, 3}, {30, 5}};
111+
auto it_end = hpx::is_sorted_until(
112+
par, fully_sorted.begin(), fully_sorted.end(), std::less<int>{}, proj);
113+
HPX_TEST(it_end == fully_sorted.end());
114+
115+
// empty range should return end (begin == end)
116+
auto it_empty = hpx::is_sorted_until(par, fully_sorted.begin(),
117+
fully_sorted.begin(), std::less<int>{}, proj);
118+
HPX_TEST(it_empty == fully_sorted.begin());
119+
}
120+
121+
// ---------------------------------------------------------------------------
122+
// is_partitioned with projection
123+
// ---------------------------------------------------------------------------
124+
void test_is_partitioned_projection()
125+
{
126+
using namespace hpx::execution;
127+
using element = std::pair<int, int>;
128+
129+
// partitioned by .second % 2 == 0: all even seconds first, then odd
130+
// second values: 2, 4, 1, 3 => partitioned (even before odd)
131+
std::vector<element> partitioned_c = {{10, 2}, {20, 4}, {30, 1}, {40, 3}};
132+
// NOT partitioned by .second % 2 == 0: 2, 1, 4, 3
133+
std::vector<element> not_partitioned_c = {
134+
{10, 2}, {20, 1}, {30, 4}, {40, 3}};
135+
136+
auto proj = [](element const& p) { return p.second; };
137+
auto pred = [](int v) { return v % 2 == 0; };
138+
139+
// sequential, no policy
140+
HPX_TEST(hpx::is_partitioned(
141+
partitioned_c.begin(), partitioned_c.end(), pred, proj));
142+
HPX_TEST(!hpx::is_partitioned(
143+
not_partitioned_c.begin(), not_partitioned_c.end(), pred, proj));
144+
145+
// seq policy
146+
HPX_TEST(hpx::is_partitioned(
147+
seq, partitioned_c.begin(), partitioned_c.end(), pred, proj));
148+
HPX_TEST(!hpx::is_partitioned(
149+
seq, not_partitioned_c.begin(), not_partitioned_c.end(), pred, proj));
150+
151+
// par policy
152+
HPX_TEST(hpx::is_partitioned(
153+
par, partitioned_c.begin(), partitioned_c.end(), pred, proj));
154+
HPX_TEST(!hpx::is_partitioned(
155+
par, not_partitioned_c.begin(), not_partitioned_c.end(), pred, proj));
156+
157+
// par(task) policy
158+
HPX_TEST(hpx::is_partitioned(
159+
par(task), partitioned_c.begin(), partitioned_c.end(), pred, proj)
160+
.get());
161+
HPX_TEST(!hpx::is_partitioned(par(task), not_partitioned_c.begin(),
162+
not_partitioned_c.end(), pred, proj)
163+
.get());
164+
165+
// empty range is always partitioned
166+
HPX_TEST(hpx::is_partitioned(
167+
par, partitioned_c.begin(), partitioned_c.begin(), pred, proj));
168+
// single element range is always partitioned
169+
HPX_TEST(hpx::is_partitioned(
170+
par, partitioned_c.begin(), partitioned_c.begin() + 1, pred, proj));
171+
}
172+
173+
int hpx_main(hpx::program_options::variables_map&)
174+
{
175+
test_is_sorted_projection();
176+
test_is_sorted_until_projection();
177+
test_is_partitioned_projection();
178+
179+
return hpx::local::finalize();
180+
}
181+
182+
int main(int argc, char* argv[])
183+
{
184+
std::vector<std::string> const cfg = {"hpx.os_threads=all"};
185+
186+
hpx::local::init_params init_args;
187+
init_args.cfg = cfg;
188+
189+
HPX_TEST_EQ_MSG(hpx::local::init(hpx_main, argc, argv, init_args), 0,
190+
"HPX main exited with non-zero status");
191+
192+
return hpx::util::report_errors();
193+
}

0 commit comments

Comments
 (0)