Skip to content

Commit 48225af

Browse files
committed
Add utility::check_strict_weak_ordering
1 parent 315fe62 commit 48225af

File tree

5 files changed

+250
-1
lines changed

5 files changed

+250
-1
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ Libraries*](https://arxiv.org/abs/1505.01962).
231231
* The algorithm behind `utility::quicksort_adversary` is a fairly straightforward adaptation of the
232232
one provided by M. D. McIlroy in [*A Killer Adversary for Quicksort*](https://www.cs.dartmouth.edu/~doug/mdmspe.pdf).
233233

234+
* The algorithm used by [`utility::check_strict_weak_ordering`][utility-check-strict-weak-ordering] is a reimplementation of the one desribed in the README file of danlark1's [quadratic_strict_weak_ordering project](https://github.com/danlark1/quadratic_strict_weak_ordering).
235+
234236
* The test suite reimplements random number algorithms originally found in the following places:
235237
- [xoshiro256\*\*](https://prng.di.unimi.it/)
236238
- [*Optimal Discrete Uniform Generation from Coin Flips, and Applications*](https://arxiv.org/abs/1304.1916)
@@ -261,3 +263,4 @@ developed by Thøger Rivera-Thorsen.
261263
[split-adapter]: https://codeberg.org/Morwenn/cpp-sort/wiki/Sorter-adapters#split_adapter
262264
[tooling]: https://codeberg.org/Morwenn/cpp-sort/wiki/Tooling
263265
[utility-as-function]: https://codeberg.org/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#as_function
266+
[utility-check-strict-weak-ordering]: https://codeberg.org/Morwenn/cpp-sort/wiki/Miscellaneous-utilities#strict-weak-ordering-checker

docs/Miscellaneous-utilities.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,31 @@ auto swap_index_pairs_force_unroll(RandomAccessIterator first,
491491
492492
`swap_index_pairs` loops over the index pairs in the simplest fashion and calls the compare-exchange operations in the simplest possible way. `swap_index_pairs_force_unroll` is a best effort function trying to achieve the same job by unrolling the loop over indices the best it can - a perfect unrolling is thus attempted, but never guaranteed, which might or might not result in faster runtime and/or increased binary size.
493493
494+
## Strict weak ordering checker
495+
496+
```cpp
497+
#include <cpp-sort/utility/check_strict_weak_ordering.h>
498+
```
499+
500+
Comparison sorting requires the comparison function to model a [strict weak ordering][strict-weak-ordering] over the values of the range to sort. Otherwise, the sorting algorithm might fail to sort the collection, or encounter even fail in hard-to-predict ways, potentially invoking undefined behavior.
501+
502+
Checking whether a comparison function models such an ordering for a given is an expensive task, which means that it is remains an unchecked precondition of algorithms in the library. If you suspect that a bug with a comparison sort might be linked to a violation of the strict weak ordering by the comparison function, you can use `utility::check_strict_weak_ordering` to analyze it over a given range:
503+
504+
```cpp
505+
std::vector<double> vec = { 1.0, 9.0, std::nan("1"), 11.5, 56.3, 2.8 };
506+
assert(not cppsort::utility::check_strict_weak_ordering(vec, std::less{});)
507+
```
508+
509+
`check_strict_weak_ordering` is a function object that follows the *unified sorting interface*: it takes a range of elements and a comparison function (and optionally a projection function). When called, it returns `true` if the passed comparison function models a strict weak ordering over the values of the input range, and `false` otherwise.
510+
511+
**WARNING: `check_strict_weak_ordering` alters the input range.**
512+
513+
| Time | Memory | Iterators |
514+
| ---- | ------ | ------------- |
515+
| n² | 1 | Random-access |
516+
517+
*New in version 2.1.0*
518+
494519
495520
[apply-permutation]: Miscellaneous-utilities.md#apply_permutation
496521
[chainable-projections]: Chainable-projections.md
@@ -524,4 +549,5 @@ auto swap_index_pairs_force_unroll(RandomAccessIterator first,
524549
[std-ranges-greater]: https://en.cppreference.com/w/cpp/utility/functional/ranges/greater
525550
[std-ranges-less]: https://en.cppreference.com/w/cpp/utility/functional/ranges/less
526551
[std-size]: https://en.cppreference.com/w/cpp/iterator/size
552+
[strict-weak-ordering]: https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings
527553
[transparent-func]: Comparators-and-projections.md#Transparent-function-objects
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright (c) 2026 Morwenn
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
#ifndef CPPSORT_UTILITY_CHECK_STRICT_WEAK_ORDERING_H_
6+
#define CPPSORT_UTILITY_CHECK_STRICT_WEAK_ORDERING_H_
7+
8+
////////////////////////////////////////////////////////////
9+
// Headers
10+
////////////////////////////////////////////////////////////
11+
#include <functional>
12+
#include <iterator>
13+
#include <utility>
14+
#include <cpp-sort/sorter_facade.h>
15+
#include <cpp-sort/sorter_traits.h>
16+
#include <cpp-sort/utility/functional.h>
17+
#include "../detail/heapsort.h"
18+
#include "../detail/is_sorted_until.h"
19+
#include "../detail/type_traits.h"
20+
21+
namespace cppsort::utility
22+
{
23+
////////////////////////////////////////////////////////////
24+
// Check whether a comparison function implements a strict
25+
// weak ordering over a range of data, following an
26+
// algorithm described by danlark1 here:
27+
// https://github.com/danlark1/quadratic_strict_weak_ordering
28+
29+
namespace detail
30+
{
31+
template<typename ForwardIterator, typename Compare, typename Projection>
32+
constexpr auto compare_equivalent(ForwardIterator it1, ForwardIterator it2,
33+
Compare compare, Projection projection)
34+
-> bool
35+
{
36+
return not compare(projection(*it1), projection(*it2))
37+
&& not compare(projection(*it2), projection(*it1));
38+
}
39+
40+
struct strict_weak_ordering_checker_impl
41+
{
42+
template<
43+
typename ForwardIterator,
44+
typename Compare = std::less<>,
45+
typename Projection = utility::identity,
46+
typename = cppsort::detail::enable_if_t<
47+
is_projection_iterator_v<Projection, ForwardIterator, Compare>
48+
>
49+
>
50+
constexpr auto operator()(ForwardIterator first, ForwardIterator last,
51+
Compare compare={}, Projection projection={}) const
52+
-> bool
53+
{
54+
// In the comments below, we use the following abbreviations:
55+
// - R is the input range
56+
// - C is the comparison function
57+
// - P is the projeciton function
58+
59+
auto&& comp = utility::as_function(compare);
60+
auto&& proj = utility::as_function(projection);
61+
62+
while (first != last) {
63+
// 1. Sort R
64+
//
65+
// Note: standard library implementations of heapsort supposedly do not
66+
// crash when passed a comparison function that does not model a strict
67+
// weak ordering, and ours happens to be copy-pasted from libc++
68+
cppsort::detail::heapsort(first, last, compare, projection);
69+
70+
// 2. If the R is not sorted, then C does not model a strict weak ordering
71+
if (not cppsort::detail::is_sorted(first, last, compare, projection)) {
72+
return false;
73+
}
74+
75+
// 3. Find first it such as *first < *it
76+
auto it = std::next(first);
77+
while (it != last && not comp(proj(*first), proj(*it))) {
78+
++it;
79+
}
80+
81+
// 4. Check that all elements before it compare equivalent
82+
for (auto it1 = first; it1 != it; ++it1) {
83+
for (auto it2 = it1; it2 != it; ++it2) {
84+
if (not compare_equivalent(it1, it2, comp, proj)) {
85+
return false;
86+
}
87+
}
88+
}
89+
90+
// 5. Check that all elements separated by it follow transitivity
91+
for (auto it1 = first; it1 != it; ++it1) {
92+
for (auto it2 = it; it2 != last; ++it2) {
93+
if (comp(proj(*it2), proj(*it1))) {
94+
return false;
95+
}
96+
if (not comp(proj(*it1), proj(*it2))) {
97+
return false;
98+
}
99+
}
100+
}
101+
102+
// Exclude leading elements that compare equivalent,
103+
// start all over again with the rest of the elements
104+
first = it;
105+
}
106+
107+
// All checks passed, C models a strict weak ordering over R
108+
return true;
109+
}
110+
111+
////////////////////////////////////////////////////////////
112+
// Sorter traits
113+
114+
using iterator_category = std::random_access_iterator_tag;
115+
};
116+
}
117+
118+
struct strict_weak_ordering_checker:
119+
sorter_facade<detail::strict_weak_ordering_checker_impl>
120+
{};
121+
122+
inline constexpr strict_weak_ordering_checker check_strict_weak_ordering{};
123+
}
124+
125+
#endif // CPPSORT_UTILITY_CHECK_STRICT_WEAK_ORDERING_H_

tests/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2015-2025 Morwenn
1+
# Copyright (c) 2015-2026 Morwenn
22
# SPDX-License-Identifier: MIT
33

44
include(cpp-sort-utils)
@@ -263,6 +263,7 @@ add_executable(main-tests
263263
utility/branchless_traits.cpp
264264
utility/buffer.cpp
265265
utility/chainable_projections.cpp
266+
utility/check_strict_weak_ordering.cpp
266267
utility/iter_swap.cpp
267268
utility/metric_tools.cpp
268269
utility/quicksort_adversary.cpp
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2026 Morwenn
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
#include <cmath>
6+
#include <vector>
7+
#include <catch2/catch_test_macros.hpp>
8+
#include <cpp-sort/utility/check_strict_weak_ordering.h>
9+
10+
TEST_CASE( "check_strict_weak_ordering test", "[utility][check_strict_weak_ordering]" )
11+
{
12+
using cppsort::utility::check_strict_weak_ordering;
13+
14+
SECTION( "empty collection" )
15+
{
16+
std::vector<int> vec = {};
17+
CHECK( check_strict_weak_ordering(vec) );
18+
}
19+
20+
SECTION( "one element" )
21+
{
22+
std::vector<int> vec = { 0 };
23+
CHECK( check_strict_weak_ordering(vec) );
24+
}
25+
26+
SECTION( "one element, NaN" )
27+
{
28+
std::vector<double> vec = { std::nan("1") };
29+
CHECK( check_strict_weak_ordering(vec) );
30+
}
31+
32+
SECTION( "empty collection" )
33+
{
34+
std::vector<int> vec = {};
35+
CHECK( check_strict_weak_ordering(vec) );
36+
}
37+
38+
SECTION( "two elements" )
39+
{
40+
std::vector<int> vec1 = { 0, 0 };
41+
CHECK( check_strict_weak_ordering(vec1) );
42+
43+
std::vector<int> vec2 = { 0, 5 };
44+
CHECK( check_strict_weak_ordering(vec2) );
45+
46+
std::vector<int> vec3 = { 5, 0 };
47+
CHECK( check_strict_weak_ordering(vec3) );
48+
}
49+
50+
SECTION( "two elements, NaN" )
51+
{
52+
std::vector<double> vec1 = { std::nan("1"), std::nan("1") };
53+
CHECK( check_strict_weak_ordering(vec1) );
54+
55+
std::vector<double> vec2 = { std::nan("1"), 5 };
56+
CHECK( check_strict_weak_ordering(vec2) );
57+
58+
std::vector<double> vec3 = { 5, std::nan("1") };
59+
CHECK( check_strict_weak_ordering(vec3) );
60+
}
61+
62+
SECTION( "small collection" )
63+
{
64+
std::vector<int> vec = { 1, 4, 32, 5, 89, 43, 56, 8, 7, 2, 44, 37, 73 };
65+
CHECK( check_strict_weak_ordering(vec) );
66+
}
67+
68+
SECTION( "small collection with duplicates" )
69+
{
70+
std::vector<int> vec = { 1, 4, 32, 5, 1, 89, 43, 56, 8, 7, 2, 2, 4, 44, 37, 73 };
71+
CHECK( check_strict_weak_ordering(vec) );
72+
}
73+
74+
SECTION( "small collection with NaN" )
75+
{
76+
std::vector<double> vec = {
77+
1.0, 4.0, 32.0, 5.0, 89.0, 43.0, 56.0, 345.0,
78+
8.0, 7.0, 2.0, std::nan("2"), 44.0, 37.0, 73.0,
79+
};
80+
CHECK_FALSE( check_strict_weak_ordering(vec) );
81+
}
82+
83+
SECTION( "small collection with std::less_equal" )
84+
{
85+
std::vector<int> vec = { 1, 4, 32, 5, 89, 43, 56, 8, 7, 2, 44, 37, 73 };
86+
CHECK_FALSE( check_strict_weak_ordering(vec, std::less_equal{}) );
87+
}
88+
89+
SECTION( "small collection with duplicates with std::less_equal" )
90+
{
91+
std::vector<int> vec = { 1, 4, 32, 5, 1, 89, 43, 56, 8, 7, 2, 2, 4, 44, 37, 73 };
92+
CHECK_FALSE( check_strict_weak_ordering(vec, std::less_equal{}) );
93+
}
94+
}

0 commit comments

Comments
 (0)