The partial_sort,
and nth_element algorithms
require the user to "manually offset" iterators,
which is neither ergonomic nor safe.
I propose _at_most variants of these algorithms
in the style of shift_left.
Contents
1. Revision history
1.1. Change since R0
2. Introduction
Algorithms such as std::ranges::partial_sort accept a "middle iterator" that
must be in the range of iterators [begin(), end()].
For example, ranges::partial_sort is specified in [partial.sort] as follows:
template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
class Proj = identity>
requires sortable<I, Comp, Proj>
constexpr I
ranges::partial_sort(I first, I middle, S last, Comp comp = {}, Proj proj = {});
template<random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R>
ranges::partial_sort(R&& r, iterator_t<R> middle, Comp comp = {}, Proj proj = {});
There are problems with this interface, described below.
2.1. Middle iterators are unsafe
A common use case for partial_sort or nth_element is to sort the
lowest elements or to obtain the lowest element,
where is an integer that is constant, obtained from a config file,
or otherwise independent of the range.
This invites a common bug,
which I have witnessed multiple times:
The following code has undefined behavior.
const int n = 10;
// ...
// usually obtained from user input, configs, etc.:
int scores[] { 5, 2, 8, 0, 3 };
// now, to obtain the 10 greatest scores:
std::partial_sort(scores,
scores + n, // 😱 undefined behavior
std::end(scores),
std::greater<int>{});
// same bug with std::ranges:
std::ranges::partial_sort(scores, scores + n, std::ranges::greater{});
The underlying problem is that to use the algorithm,
we need to offset the start iterator,
and this can fall outside of the range of iterators [begin(), end()].
A possible safe alternative which would have prevented the bug above is
std::ranges::partial_sort(scores,
std::ranges::next(scores, n, std::ranges::end(scores)),
std::ranges::greater{});
However, this is neither ergonomic nor does it address the cause of the bug.
The bug was caused by the user not remembering that scores + n can be undefined,
and by the interface encouraging them to write this expression.
Why should we expect the user to know of and consistently use this workaround then?
Users generally prefer to use to "path of least resistance",
and we should make that path safe:
// OK, even if n is greater than the size of the range
std::ranges::partial_sort_at_most(scores, n, std::ranges::greater{});
2.2. Middle iterators are annoying
Since we need to obtain the middle iterator from the range
but also provide the range to partial_sort,
we may have to repeat ourselves.
Say we want to obtain the lowest scores similar to the bug above,
but the scores are obtained from a function instead:
namespace rgs = std::ranges;
some_range& long_function_to_get_scores_range();
// without a variable:
rgs::partial_sort(long_function_to_get_scores_range(),
rgs::next(rgs::begin(long_function_to_get_scores_range()), n, // 🤡 seriously?
rgs::end(long_function_to_get_scores_range())),
rgs::greater{});
// with a variable:
some_range& r = long_function_to_get_scores_range();
rgs::partial_sort(r, rgs::next(rgs::begin(r), n, rgs::end(r)), rgs::greater{});
Either options is somewhat annoying if it could theoretically be written as:
rgs::partial_sort_at_most(long_function_to_get_scores_range(), n, rgs::greater{});
One key benefit of std::ranges is that we rarely have to work with
iterators directly anymore.
Requiring the user to provide middle iterators somewhat defeats this design strategy.
3. Scope
To address the problems above,
I propose the following additional algorithms:
partial_sort_at_most
nth_element_at_most
These should exist both for the std::ranges:: and the std:: algorithms,
as they provide a "safety/ergonomics hotfix" for anyone
who has not migrated to these "new" algorithms yet,
and is perhaps unable to
because their iterators would not satisfy the std::ranges constraints.
While there are more algorithms that accept middle iterators,
they do not exclusively operate on random access iterators,
which results in different design concerns.
There is also no proposed overload for partial_sort_copy
because its result can already be specified as a range or iterator pair,
so the concerns in §2.1. Middle iterators are unsafe do not apply.
During Kona 2025 review of R0 of this paper by SG9,
the opinion of room was that other algorithms
could be addressed in a separate papers.
4. Design
The design of the function signatures
is essentially copied from shift_left.
The proposed functions take either an iterator pair and a size,
or a range and a size,
just like shift_left and shift_right, and with similar interface.
See [alg.shift].
4.1. Naming
The naming is novel.
R0 of this paper proposed _n suffixes instead of _at_most,
but this was unpopular in SG9.
The issue with _n is that this suffix is used to mean
"exactly N", not "at most" (with clamping/views::take-like) behavior.
A few possible candidates were discussed,
and the currently proposed one is simply the most popular:
Approval of naming scheme:
| Name |
Votes |
_n | 1 |
_at_most | 7 |
_up_to | 5 |
_clamped | 3 |
_safe | 0 |
Attendance: 7
4.2. Additional variants of partial_sort
In addition to the "clamping" behavior of the proposed functions,
SG9 also expressed interest in functions that detect misuses of partial_sort
as a hardened precondition or by throwing an exception:
We want a variant of partial_sort/nth_element
that takes a count n instead of an iterator mid
and has a precondition/hardened precondition/erroneous behavior/throws an exception
if n is too large.
Author: A
Attendance: 7
Consensus in favor.
However, just because there is interest,
doesn't mean these functions need to be in this paper.
Such new functions would have to be separate overload sets because
adding additional "safer" overloads to partial_sort
- bloats the size of the overload set, and
-
creates an ambiguity between passing a null pointer constant
0
(where the iterator is a pointer, for a contiguous range)
and a 0 that is convertible to a difference_type,
when 0 is passed as the size argument to the algorithm.
However, creating an entirely new overload set
just to add a range check is novel and arguably overkill;
I believe it would decrease consensus on this proposal.
5. Impact on existing code
No existing code should be affected because the proposed functions are all new.
Existing overload sets are not modified.
6. Wording
The proposed changes are relative to [N5014].
In [version.syn],
add two feature-test macros as follows:
#define __cpp_lib_ranges_partial_sort_at_most 20XXXXL // also in <algorithm>
#define __cpp_lib_ranges_nth_element_at_most 20XXXXL // also in <algorithm>
In [algorithm.syn],
change the synopsis of <algorithm> as follows:
namespace std {
[…]
template<class RandomAccessIterator>
constexpr void partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
RandomAccessIterator last);
template<class RandomAccessIterator, class Compare>
constexpr void partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
RandomAccessIterator last, Compare comp);
template<class ExecutionPolicy, class RandomAccessIterator>
void partial_sort(ExecutionPolicy&& exec, // freestanding-deleted, see [algorithms.parallel.overloads]
RandomAccessIterator first, RandomAccessIterator middle,
RandomAccessIterator last);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void partial_sort(ExecutionPolicy&& exec, // freestanding-deleted, see [algorithms.parallel.overloads]
RandomAccessIterator first, RandomAccessIterator middle,
RandomAccessIterator last, Compare comp);
namespace ranges {
template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
class Proj = identity>
requires sortable<I, Comp, Proj>
constexpr I
partial_sort(I first, I middle, S last, Comp comp = {}, Proj proj = {});
template<random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R>
partial_sort(R&& r, iterator_t<R> middle, Comp comp = {},
Proj proj = {});
}
template<class RandomAccessIterator>
void partial_sort_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void partial_sort_at_most(ExecutionPolicy&& exec, // freestanding-deleted, see [algorithms.parallel.overloads]
RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class RandomAccessIterator, class Compare>
void partial_sort_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void partial_sort_at_most(ExecutionPolicy&& exec, // freestanding-deleted, see [algorithms.parallel.overloads]
RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
namespace ranges {
template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
class Proj = identity>
requires sortable<I, Comp, Proj>
constexpr I partial_sort_at_most(I first, iter_difference_t<I> n, S last,
Comp comp = {}, Proj proj = {});
template<random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R> partial_sort_at_most(R&& r,
ranges::range_difference_t<R> n,
Comp comp = {}, Proj proj = {});
}
[…]
// [alg.nth.element], Nth element
template<class RandomAccessIterator>
constexpr void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last);
template<class RandomAccessIterator, class Compare>
constexpr void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last, Compare comp);
template<class ExecutionPolicy, class RandomAccessIterator>
void nth_element(ExecutionPolicy&& exec, // freestanding-deleted, see [algorithms.parallel.overloads]
RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void nth_element(ExecutionPolicy&& exec, // freestanding-deleted, see [algorithms.parallel.overloads]
RandomAccessIterator first, RandomAccessIterator nth,
RandomAccessIterator last, Compare comp);
namespace ranges {
template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
class Proj = identity>
requires sortable<I, Comp, Proj>
constexpr I
nth_element(I first, I nth, S last, Comp comp = {}, Proj proj = {});
template<random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R>
nth_element(R&& r, iterator_t<R> nth, Comp comp = {}, Proj proj = {});
}
template<class RandomAccessIterator>
void nth_element_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void nth_element_at_most(ExecutionPolicy&& exec, RandomAccessIterator first,
RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class RandomAccessIterator, class Compare>
void nth_element_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void nth_element_at_most(ExecutionPolicy&& exec, RandomAccessIterator first,
RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
namespace ranges {
template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
class Proj = identity>
requires sortable<I, Comp, Proj>
constexpr I nth_element_at_most(I first, S last, iter_difference_t<I> n,
Comp comp = {}, Proj proj = {});
template<random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R> nth_element_at_most(R&& r,
ranges::range_difference_t<R> n,
Comp comp = {}, Proj proj = {});
}
[…]
}
In [partial.sort],
append the following items at the end of the subclause:
template<class RandomAccessIterator>
void partial_sort_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void partial_sort_at_most(ExecutionPolicy&& exec,
RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class RandomAccessIterator, class Compare>
void partial_sort_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void partial_sort_at_most(ExecutionPolicy&& exec,
RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
Effects:
Equivalent to calling partial_sort
with all parameters excluding n in the same order,
except that first + min(n, last - first) is
provided as an argument for the middle parameter in partial_sort.
template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
class Proj = identity>
requires sortable<I, Comp, Proj>
constexpr I ranges::partial_sort_at_most(I first, iter_difference_t<I> n, S last,
Comp comp = {}, Proj proj = {});
Effects:
Equivalent to:
return ranges::partial_sort(first, ranges::next(first, n, last), last, comp, proj);
template<random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R> ranges::partial_sort_n(R&& r,
ranges::range_difference_t<R> n,
Comp comp = {}, Proj proj = {});
Effects:
Equivalent to:
return ranges::partial_sort_at_most(ranges::begin(r), n, ranges::end(r), comp, proj);
In [alg.nth.element],
append the following items at the end of the subclause:
template<class RandomAccessIterator>
void nth_element_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void nth_element_at_most(ExecutionPolicy&& exec, RandomAccessIterator first,
RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n);
template<class RandomAccessIterator, class Compare>
void nth_element_at_most(RandomAccessIterator first, RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
template<class ExecutionPolicy, class RandomAccessIterator, class Compare>
void nth_element_at_most(ExecutionPolicy&& exec, RandomAccessIterator first,
RandomAccessIterator last,
typename iterator_traits<RandomAccessIterator>::difference_type n,
Compare comp);
Effects:
Equivalent to calling nth_element
with all parameters excluding n in the same order,
except that first + min(n, last - first) is
provided as an argument for the nth parameter in nth_element.
template<random_access_iterator I, sentinel_for<I> S, class Comp = ranges::less,
class Proj = identity>
requires sortable<I, Comp, Proj>
constexpr I ranges::nth_element_at_most(I first, S last, iter_difference_t<I> n,
Comp comp = {}, Proj proj = {});
Effects:
Equivalent to:
return ranges::nth_element(first, ranges::next(first, n, last), last, comp, proj);
template<random_access_range R, class Comp = ranges::less, class Proj = identity>
requires sortable<iterator_t<R>, Comp, Proj>
constexpr borrowed_iterator_t<R> ranges::nth_element_at_most(R&& r,
ranges::range_difference_t<R> n,
Comp comp = {}, Proj proj = {});
Effects:
Equivalent to:
return ranges::nth_element_at_most(ranges::begin(r), n, ranges::end(r), comp, proj);
7. References