partial_sort_at_most, nth_element_at_most

Document number:
P3735R1
Date:
2025-11-17
Audience:
LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Reply-to:
Jan Schultke <janschultke@gmail.com>
GitHub Issue:
wg21.link/P3735/github
Source:
github.com/Eisenwave/cpp-proposals/blob/master/src/n-algorithms.cow

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

2.1

Middle iterators are unsafe

2.2

Middle iterators are annoying

3

Scope

4

Design

4.1

Naming

4.2

Additional variants of partial_sort

5

Impact on existing code

6

Wording

7

References

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 N lowest elements or to obtain the Nth lowest element, where N 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 N 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:

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.

SFFNASA
05020

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

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

[N5014] Thomas Köppe. Working Draft, Programming Languages — C++ 2025-08-05 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5014.pdf