I am continuing to go through the sus::iter::Iterator methods and turn them all into constexpr based on the performance results from doing so. With constexpr an iterator of chunks_exact(), and take_while() is able to outperform a “bare-metal” loop over pointers. More on that over at Mastodon.

As I am doing so, I am writing tests, and this test felt really good to write!

static_assert(sus::Vec<f32>::with(2.f, 3.f, 2.f)
    .into_iter()
    .max_by(&f32::total_cmp) == sus::some(3.f));

So I interrogated myself why it did. First off, I was happy that you can’t write this wrong. You can’t call max() on floats because they are not strongly ordered. In other words, operator<=>() on floats returns std::partial_ordering, not std::strong_ordering. That means that floats satisfy the PartialOrd concept but not the Ord concept. And max() requires Ord.

This is true for float as well, but f32 is nicer to work with for reasons like the NAN constant and that is_nan() is constexpr unlike std::isnan() until C++23.

// Doesn't compile, as floats are not strongly ordered. Constraint does not match.
// error C7500: 'max': no function satisfied its constraints
sus::Vec<f32>::with(2.f, 3.f, 2.f).into_iter().max();

The f32::total_cmp() method provides a total ordering over floats and does return std::strong_ordering. It does the same thing as f32::total_cmp in Rust std. So we can use it as a callback in max_by().

What does the standard ranges library do when you try to max floats, I wondered. At least on MSVC this is what I got.

// Ok sure, looks good.
static_assert(std::ranges::max(
  std::vector<f32>({2.f, 3.f, 2.f})) == 3.f);
// At compile time, NAN is largest.
static_assert(std::ranges::max(
  std::vector<f32>({2.f, 3.f, f32::NAN})).is_nan());
// Unless 3 comes after.
static_assert(std::ranges::max(
  std::vector<f32>({f32::NAN, 3.f, 2.f})) == 3.f);
// At run time, different answers. 3 is largest if it comes first.
EXPECT_EQ(std::ranges::max(
  std::vector<f32>({2.f, 3.f, f32::NAN})), 3.f);
// At run time, NAN is larger if it comes first.
EXPECT_EQ(std::ranges::max(
  std::vector<f32>({f32::NAN, 3.f, 2.f})).is_nan(), true);
// Undefined behaviour!!!
EXPECT_EQ(
  std::ranges::max(std::vector<f32>()), 0.f);

Those results are really not okay! They are the kind of thing that doesn’t show up in tests and then completely takes down production. Maybe introduces a security vuln.

Here’s what I got with Subspace iterators.

// Reproducible ordering.
static_assert(sus::Vec<f32>::with(2.f, 3.f, 2.f)
  .into_iter()
  .max_by(&f32::total_cmp) == sus::some(3.f));
static_assert(sus::Vec<f32>::with(2.f, 3.f, f32::NAN)
  .into_iter()
  .max_by(&f32::total_cmp).unwrap().is_nan());
static_assert(sus::Vec<f32>::with(f32::NAN, 3.f, 2.f)
  .into_iter()
  .max_by(&f32::total_cmp).unwrap().is_nan());
// Same thing at runtime as at compile time.
EXPECT_EQ(sus::Vec<f32>::with(2.f, 3.f, f32::NAN)
  .into_iter()
  .max_by(&f32::total_cmp).unwrap().is_nan(), true);
EXPECT_EQ(sus::Vec<f32>::with(f32::NAN, 3.f, 2.f)
  .into_iter()
  .max_by(&f32::total_cmp).unwrap().is_nan(), true);
// Defined behaviour. ^_^b
static_assert(sus::Vec<f32>::with()
  .into_iter()
  .max_by(&f32::total_cmp) == sus::none());

Sure you can just write a total_cmp() method yourself and use that as the comparator with the standard ranges library. But you don’t have to, it compiles anyway so there’s nothing suggesting something is wrong. And then it returns garbage or corrupts your compilation with Undefined Behaviour.

With Subspace total_cmp() is already there for you, and non-sense inputs don’t compile.

This stuff is why Rust is pleasant to work in. C++ can be too.