Skip to content

Commit b49b5ec

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

File tree

5 files changed

+253
-1
lines changed

5 files changed

+253
-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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
#include <cassert>
22+
23+
namespace cppsort::utility
24+
{
25+
////////////////////////////////////////////////////////////
26+
// Check whether a comparison function implements a strict
27+
// weak ordering over a range of data, following an
28+
// algorithm described by danlark1 here:
29+
// https://github.com/danlark1/quadratic_strict_weak_ordering
30+
31+
namespace detail
32+
{
33+
template<typename ForwardIterator, typename Compare, typename Projection>
34+
constexpr auto compare_equivalent(ForwardIterator it1, ForwardIterator it2,
35+
Compare compare, Projection projection)
36+
-> bool
37+
{
38+
return not compare(projection(*it1), projection(*it2))
39+
&& not compare(projection(*it2), projection(*it1));
40+
}
41+
42+
struct strict_weak_ordering_checker_impl
43+
{
44+
template<
45+
typename ForwardIterator,
46+
typename Compare = std::less<>,
47+
typename Projection = utility::identity,
48+
typename = cppsort::detail::enable_if_t<
49+
is_projection_iterator_v<Projection, ForwardIterator, Compare>
50+
>
51+
>
52+
constexpr auto operator()(ForwardIterator first, ForwardIterator last,
53+
Compare compare={}, Projection projection={}) const
54+
-> bool
55+
{
56+
// In the comments below, we use the following abbreviations:
57+
// - R is the input range
58+
// - C is the comparison function
59+
// - P is the projeciton function
60+
61+
auto&& comp = utility::as_function(compare);
62+
auto&& proj = utility::as_function(projection);
63+
64+
while (first != last) {
65+
// 1. Sort R
66+
//
67+
// Note: standard library implementations of heapsort supposedly do not
68+
// crash when passed a comparison function that does not model a strict
69+
// weak ordering, and ours happens to be copy-pasted from libc++
70+
cppsort::detail::heapsort(first, last, compare, projection);
71+
72+
// 2. If the R is not sorted, then C does not model a strict weak ordering
73+
if (not cppsort::detail::is_sorted(first, last, compare, projection)) {
74+
return false;
75+
}
76+
77+
// 3. Find first it such as *first < *it
78+
auto it = std::next(first);
79+
while (it != last && not comp(proj(*first), proj(*it))) {
80+
++it;
81+
}
82+
83+
// 4. Check that all elements before it compare equivalent
84+
for (auto it1 = first; it1 != it; ++it1) {
85+
for (auto it2 = it1; it2 != it; ++it2) {
86+
if (not compare_equivalent(it1, it2, comp, proj)) {
87+
return false;
88+
}
89+
}
90+
}
91+
92+
// 5. Check that all elements separated by it follow transitivity
93+
for (auto it1 = first; it1 != it; ++it1) {
94+
for (auto it2 = it; it2 != last; ++it2) {
95+
if (comp(proj(*it2), proj(*it1))) {
96+
return false;
97+
}
98+
if (not comp(proj(*it1), proj(*it2))) {
99+
return false;
100+
}
101+
}
102+
}
103+
104+
// Exclude leading elements that compare equivalent,
105+
// start all over again with the rest of the elements
106+
first = it;
107+
}
108+
109+
// All checks passed, C models a strict weak ordering over R
110+
return true;
111+
}
112+
113+
////////////////////////////////////////////////////////////
114+
// Sorter traits
115+
116+
using iterator_category = std::random_access_iterator_tag;
117+
};
118+
}
119+
120+
struct strict_weak_ordering_checker:
121+
sorter_facade<detail::strict_weak_ordering_checker_impl>
122+
{};
123+
124+
inline constexpr strict_weak_ordering_checker check_strict_weak_ordering{};
125+
}
126+
127+
#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: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
#include <testing-tools/distributions.h>
10+
11+
TEST_CASE( "check_strict_weak_ordering test", "[utility][check_strict_weak_ordering]" )
12+
{
13+
using cppsort::utility::check_strict_weak_ordering;
14+
15+
SECTION( "empty collection" )
16+
{
17+
std::vector<int> vec = {};
18+
CHECK( check_strict_weak_ordering(vec) );
19+
}
20+
21+
SECTION( "one element" )
22+
{
23+
std::vector<int> vec = { 0 };
24+
CHECK( check_strict_weak_ordering(vec) );
25+
}
26+
27+
SECTION( "one element, NaN" )
28+
{
29+
std::vector<double> vec = { std::nan("1") };
30+
CHECK( check_strict_weak_ordering(vec) );
31+
}
32+
33+
SECTION( "empty collection" )
34+
{
35+
std::vector<int> vec = {};
36+
CHECK( check_strict_weak_ordering(vec) );
37+
}
38+
39+
SECTION( "two elements" )
40+
{
41+
std::vector<int> vec1 = { 0, 0 };
42+
CHECK( check_strict_weak_ordering(vec1) );
43+
44+
std::vector<int> vec2 = { 0, 5 };
45+
CHECK( check_strict_weak_ordering(vec2) );
46+
47+
std::vector<int> vec3 = { 5, 0 };
48+
CHECK( check_strict_weak_ordering(vec3) );
49+
}
50+
51+
SECTION( "two elements, NaN" )
52+
{
53+
std::vector<double> vec1 = { std::nan("1"), std::nan("1") };
54+
CHECK( check_strict_weak_ordering(vec1) );
55+
56+
std::vector<double> vec2 = { std::nan("1"), 5 };
57+
CHECK( check_strict_weak_ordering(vec2) );
58+
59+
std::vector<double> vec3 = { 5, std::nan("1") };
60+
CHECK( check_strict_weak_ordering(vec3) );
61+
}
62+
63+
SECTION( "small collection" )
64+
{
65+
std::vector<int> vec = { 1, 4, 32, 5, 89, 43, 56, 8, 7, 2, 44, 37, 73 };
66+
CHECK( check_strict_weak_ordering(vec) );
67+
}
68+
69+
SECTION( "small collection with duplicates" )
70+
{
71+
std::vector<int> vec = { 1, 4, 32, 5, 1, 89, 43, 56, 8, 7, 2, 2, 4, 44, 37, 73 };
72+
CHECK( check_strict_weak_ordering(vec) );
73+
}
74+
75+
SECTION( "small collection with NaN" )
76+
{
77+
std::vector<double> vec = {
78+
1.0, 4.0, 32.0, 5.0, 89.0, 43.0, 56.0, 345.0,
79+
8.0, 7.0, 2.0, std::nan("2"), 44.0, 37.0, 73.0,
80+
};
81+
CHECK_FALSE( check_strict_weak_ordering(vec) );
82+
}
83+
84+
SECTION( "small collection with std::less_equal" )
85+
{
86+
std::vector<int> vec = { 1, 4, 32, 5, 89, 43, 56, 8, 7, 2, 44, 37, 73 };
87+
CHECK_FALSE( check_strict_weak_ordering(vec, std::less_equal{}) );
88+
}
89+
90+
SECTION( "small collection with duplicates with std::less_equal" )
91+
{
92+
std::vector<int> vec = { 1, 4, 32, 5, 1, 89, 43, 56, 8, 7, 2, 2, 4, 44, 37, 73 };
93+
CHECK_FALSE( check_strict_weak_ordering(vec, std::less_equal{}) );
94+
}
95+
}

0 commit comments

Comments
 (0)