Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5fd7338
[parallel] Fix hpx::nth_element to correctly support projections and …
dasaneek007-cpu Apr 16, 2026
181ec0a
Apply clang-format fixes
dasaneek007-cpu Apr 16, 2026
f895d19
Merge branch 'master' into fix/nth-element-projections-sentinels
aneek22112007-tech Apr 16, 2026
b0ebd62
Remove <iostream> inclusion to fix hpxinspect failure
dasaneek007-cpu Apr 16, 2026
9740280
Revert hpx::nth_element to standard compliance, move projections/sent…
dasaneek007-cpu Apr 16, 2026
e58f6aa
Address maintainer feedback: consolidate wrapped_comp_type and use st…
dasaneek007-cpu Apr 16, 2026
8199058
Apply clang-format fixes
dasaneek007-cpu Apr 16, 2026
5590bc1
Fix macOS CI: avoid detail::min_element type-matching issue with proj…
dasaneek007-cpu Apr 17, 2026
891e829
Merge branch 'master' into fix/nth-element-projections-sentinels
aneek22112007-tech Apr 17, 2026
5f9a80b
Fix type deduction in minmax.hpp and remove workaround in nth_element…
dasaneek007-cpu Apr 17, 2026
e61e974
Apply clang-format corrections to minmax.hpp
dasaneek007-cpu Apr 17, 2026
3ad02be
Fix clang-format in minmax.hpp after CI failure
dasaneek007-cpu Apr 17, 2026
c860663
Fix clang-format in minmax.hpp (final attempt)
dasaneek007-cpu Apr 17, 2026
e993817
Automated clang-format fix using opt/homebrew/bin/clang-format
dasaneek007-cpu Apr 17, 2026
30cf465
Refine element_type deduction to handle HPX proxies using proxy_value_t
dasaneek007-cpu Apr 18, 2026
0e2e53e
Remove redundant typename keywords as requested by maintainer
dasaneek007-cpu Apr 19, 2026
865d418
Restore typename keywords (strictly required for CI builds)
dasaneek007-cpu Apr 19, 2026
a4b4a86
Separate core fix into independent PR and restore temporary workaround
dasaneek007-cpu Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 104 additions & 24 deletions libs/core/algorithms/include/hpx/parallel/algorithms/nth_element.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,14 @@ namespace hpx {
#include <hpx/modules/executors.hpp>
#include <hpx/modules/functional.hpp>
#include <hpx/modules/iterator_support.hpp>
#include <hpx/parallel/algorithms/detail/advance_to_sentinel.hpp>
#include <hpx/parallel/algorithms/detail/dispatch.hpp>
#include <hpx/parallel/algorithms/detail/pivot.hpp>
#include <hpx/parallel/algorithms/make_heap.hpp>
#include <hpx/parallel/algorithms/minmax.hpp>
#include <hpx/parallel/algorithms/partial_sort.hpp>
#include <hpx/parallel/algorithms/partition.hpp>
#include <hpx/parallel/util/compare_projected.hpp>
#include <hpx/parallel/util/detail/algorithm_result.hpp>
#include <hpx/parallel/util/detail/sender_util.hpp>

Expand Down Expand Up @@ -186,9 +189,12 @@ namespace hpx::parallel {
// Check the special conditions
if (nth == first)
{
using wrapped_comp_type =
hpx::parallel::util::compare_projected<
std::decay_t<Compare>, std::decay_t<Proj>>;
RandomIt it = detail::min_element<RandomIt>().call(
hpx::execution::seq, first, end, HPX_FORWARD(Compare, comp),
HPX_FORWARD(Proj, proj));
hpx::execution::seq, first, end,
wrapped_comp_type(comp, proj), hpx::identity_v);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the rationale of this change?

Copy link
Copy Markdown
Contributor Author

@aneek22112007-tech aneek22112007-tech Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that the underlying detail::min_element implementation in minmax.hpp actually fails on macOS Clang when a projection changes the value type (it tries to store the result in a variable of the original element type). Using the wrapped comparison here is a necessary workaround to keep the macOS builds green.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to fix the actual problem instead of adding a workaround here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you be able to fix this problem (in an independent PR)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've followed your suggestion and separated the core fix for minmax.hpp into an independent Pull Request. You can find it here: #7208.

On this current branch, I have:

Reverted the changes to minmax.hpp to match master.
Restored the wrapped_comp_type workaround in nth_element.hpp specifically for the min_element call.
This keeps this PR functional and 'Green' in CI while the core fix is being reviewed. Once the independent minmax PR is merged, I will come back here to remove the temporary workaround. Let me know if that works for you!


if (it != first)
{
Expand All @@ -200,19 +206,30 @@ namespace hpx::parallel {

if (nelem < nmin_sort)
{
using wrapped_comp_type =
hpx::parallel::util::compare_projected<
std::decay_t<Compare>, std::decay_t<Proj>>;
detail::sort<RandomIt>().call(hpx::execution::seq, first, end,
HPX_FORWARD(Compare, comp), HPX_FORWARD(Proj, proj));
wrapped_comp_type(comp, proj), hpx::identity_v);
return;
}
if (level == 0)
{
std::make_heap(first, end, comp);
std::sort_heap(first, nth, comp);
using wrapped_comp_type =
hpx::parallel::util::compare_projected<
std::decay_t<Compare>, std::decay_t<Proj>>;
hpx::parallel::detail::make_heap<RandomIt>().call(
hpx::execution::seq, first, end,
wrapped_comp_type(comp, proj), hpx::identity_v);
Comment thread
hkaiser marked this conversation as resolved.
Outdated
std::sort_heap(first, nth, wrapped_comp_type(comp, proj));
return;
}

// Filter the range and check which part contains the nth element
RandomIt c_last = filter(first, end, comp);
using wrapped_comp_type =
Comment thread
hkaiser marked this conversation as resolved.
Outdated
hpx::parallel::util::compare_projected<std::decay_t<Compare>,
std::decay_t<Proj>>;
RandomIt c_last = filter(first, end, wrapped_comp_type(comp, proj));

if (c_last == nth)
return;
Expand Down Expand Up @@ -263,9 +280,6 @@ namespace hpx::parallel {
parallel(ExPolicy&& policy, RandomIt first, RandomIt nth, Sent last,
Pred&& pred, Proj&& proj)
{
using value_type =
typename std::iterator_traits<RandomIt>::value_type;

RandomIt partition_iter, return_last;

if (first == last)
Expand All @@ -288,17 +302,21 @@ namespace hpx::parallel {

while (first != last_iter)
{
detail::pivot9(first, last_iter, pred);
detail::pivot9(first, last_iter,
hpx::parallel::util::compare_projected<
std::decay_t<Pred>, std::decay_t<Proj>>(
pred, proj));

partition_iter =
hpx::parallel::detail::partition<RandomIt>().call(
policy(hpx::execution::non_task), first + 1,
last_iter,
[val = HPX_INVOKE(proj, *first), &pred](
value_type const& elem) {
return HPX_INVOKE(pred, elem, val);
[val = HPX_INVOKE(proj, *first), &pred, &proj](
auto const& elem) {
return HPX_INVOKE(
pred, HPX_INVOKE(proj, elem), val);
},
proj);
hpx::identity_v);

--partition_iter;

Expand Down Expand Up @@ -347,42 +365,104 @@ namespace hpx {
: hpx::detail::tag_parallel_algorithm<nth_element_t>
{
template <typename RandomIt,
typename Pred = hpx::parallel::detail::less>
typename Pred = hpx::parallel::detail::less,
typename Proj = hpx::identity>
// clang-format off
requires (
hpx::traits::is_iterator_v<RandomIt> &&
hpx::is_invocable_v<Pred,
typename std::iterator_traits<RandomIt>::value_type,
typename std::iterator_traits<RandomIt>::value_type
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type,
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type
>
)
// clang-format on
friend void tag_fallback_invoke(hpx::nth_element_t, RandomIt first,
RandomIt nth, RandomIt last, Pred pred = Pred())
RandomIt nth, RandomIt last, Pred pred = Pred(), Proj proj = Proj())
Comment thread
hkaiser marked this conversation as resolved.
Outdated
{
static_assert(std::random_access_iterator<RandomIt>,
"Requires at least random iterator.");

hpx::parallel::detail::nth_element<RandomIt>().call(
hpx::execution::seq, first, nth, last, HPX_MOVE(pred),
hpx::identity_v);
HPX_MOVE(proj));
}

template <typename RandomIt, typename Sent, typename Pred,
typename Proj>
// clang-format off
requires (
hpx::traits::is_iterator_v<RandomIt> &&
hpx::is_invocable_v<Pred,
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type,
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type
>
)
// clang-format on
friend void tag_fallback_invoke(hpx::nth_element_t, RandomIt first,
RandomIt nth, Sent last, Pred pred = Pred(), Proj proj = Proj())
{
static_assert(std::random_access_iterator<RandomIt>,
"Requires at least random iterator.");

hpx::parallel::detail::nth_element<RandomIt>().call(
hpx::execution::seq, first, nth, last, HPX_MOVE(pred),
HPX_MOVE(proj));
}

template <typename ExPolicy, typename RandomIt,
typename Pred = hpx::parallel::detail::less>
typename Pred = hpx::parallel::detail::less,
typename Proj = hpx::identity>
// clang-format off
requires (
hpx::is_execution_policy_v<ExPolicy> &&
hpx::traits::is_iterator_v<RandomIt> &&
hpx::is_invocable_v<Pred,
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type,
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type
>
)
// clang-format on
friend parallel::util::detail::algorithm_result_t<ExPolicy>
tag_fallback_invoke(hpx::nth_element_t, ExPolicy&& policy,
RandomIt first, RandomIt nth, RandomIt last, Pred pred = Pred(),
Proj proj = Proj())
{
static_assert(std::random_access_iterator<RandomIt>,
"Requires at least random iterator.");

using result_type =
hpx::parallel::util::detail::algorithm_result_t<ExPolicy>;

return hpx::util::void_guard<result_type>(),
hpx::parallel::detail::nth_element<RandomIt>().call(
HPX_FORWARD(ExPolicy, policy), first, nth, last,
HPX_MOVE(pred), HPX_MOVE(proj));
}

template <typename ExPolicy, typename RandomIt, typename Sent,
typename Pred, typename Proj>
// clang-format off
requires (
hpx::is_execution_policy_v<ExPolicy> &&
hpx::traits::is_iterator_v<RandomIt> &&
hpx::is_invocable_v<Pred,
typename std::iterator_traits<RandomIt>::value_type,
typename std::iterator_traits<RandomIt>::value_type
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type,
typename hpx::parallel::traits::projected_result_of<Proj,
RandomIt>::type
>
)
// clang-format on
friend parallel::util::detail::algorithm_result_t<ExPolicy>
tag_fallback_invoke(hpx::nth_element_t, ExPolicy&& policy,
RandomIt first, RandomIt nth, RandomIt last, Pred pred = Pred())
RandomIt first, RandomIt nth, Sent last, Pred pred = Pred(),
Proj proj = Proj())
{
static_assert(std::random_access_iterator<RandomIt>,
"Requires at least random iterator.");
Expand All @@ -393,7 +473,7 @@ namespace hpx {
return hpx::util::void_guard<result_type>(),
hpx::parallel::detail::nth_element<RandomIt>().call(
HPX_FORWARD(ExPolicy, policy), first, nth, last,
HPX_MOVE(pred), hpx::identity_v);
HPX_MOVE(pred), HPX_MOVE(proj));
}
} nth_element{};
} // namespace hpx
Expand Down
1 change: 1 addition & 0 deletions libs/core/algorithms/tests/unit/algorithms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ set(tests
mismatch_binary
move
nth_element
nth_element_projection
none_of
parallel_sort
partial_sort
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) 2014 Grant Mercer
// Copyright (c) 2017-2024 Hartmut Kaiser
//
// SPDX-License-Identifier: BSL-1.0
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

#include <hpx/algorithm.hpp>
#include <hpx/init.hpp>
#include <hpx/modules/testing.hpp>

#include <cstddef>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

struct S
{
int val;
};

template <typename ExPolicy>
void test_nth_element_projection(ExPolicy policy)
{
std::vector<S> v(100);
for (int i = 0; i < 100; ++i)
v[i].val = 100 - i;

auto nth = v.begin() + 50;

hpx::nth_element(
policy, v.begin(), nth, v.end(), std::less<int>{}, &S::val);

// After nth_element, the element at nth should be the 51st smallest element (which is 51)
HPX_TEST_EQ(v[50].val, 51);

// All elements before nth should be <= v[50].val
for (auto it = v.begin(); it != nth; ++it)
{
HPX_TEST_LTE(it->val, v[50].val);
}

// All elements after nth should be >= v[50].val
for (auto it = nth + 1; it != v.end(); ++it)
{
HPX_TEST(it->val >= v[50].val);
}
}

template <typename ExPolicy>
void test_nth_element_projection_type_change(ExPolicy policy)
{
std::vector<std::string> v = {"abc", "a", "abcd", "ab", "abcde"};
auto nth = v.begin() + 2;

// Sort by string length
hpx::nth_element(policy, v.begin(), nth, v.end(), std::less<std::size_t>{},
&std::string::length);

HPX_TEST_EQ(v[2].length(), std::size_t(3));
}

int hpx_main()
{
test_nth_element_projection(hpx::execution::seq);
test_nth_element_projection(hpx::execution::par);
test_nth_element_projection(hpx::execution::par_unseq);

test_nth_element_projection_type_change(hpx::execution::seq);
test_nth_element_projection_type_change(hpx::execution::par);
test_nth_element_projection_type_change(hpx::execution::par_unseq);

return hpx::local::finalize();
}

int main(int argc, char* argv[])
{
std::vector<std::string> const cfg = {"hpx.os_threads=all"};

hpx::local::init_params init_args;
init_args.cfg = cfg;

HPX_TEST_EQ_MSG(hpx::local::init(hpx_main, argc, argv, init_args), 0,
"HPX main exited with non-zero status");

return hpx::util::report_errors();
}
Loading