std::ranges...nmax_result(3)

πŸ“¦ version 2024.07.05, in nixpkgs unstable, opensuse-leap 16.0

✨ NAME ✨

std::ranges::minmax, std::ranges::minmax_result β€” bestie, this is the C++ Standard Library page for them, straight out of the manual (section 3, iykyk). It's double the slay: one call that hands you back bof the smallest and the greatest at the same time. Iconic behavior, honestly.


πŸ¦‹ Synopsis πŸ¦‹

Here's the vibe, verbatim, no cap:

          Defined in header <algorithm>
          Call signature
          template< class T, class Proj = std::identity,

                    std::indirect_strict_weak_order<
       (since
                        std::projected<const T*, Proj>> Comp = ranges::less >
       (1) C++20)
          constexpr ranges::minmax_result<const T&>

              minmax( const T& a, const T& b, Comp comp = {}, Proj proj = {} );
          template< std::copyable T, class Proj = std::identity,

                    std::indirect_strict_weak_order<
                        std::projected<const T*, Proj>> Comp = ranges::less >
       (2) (since
          constexpr ranges::minmax_result<T>
       C++20)

              minmax( std::initializer_list<T> r, Comp comp = {}, Proj proj =
       {}
          );
          template< ranges::input_range R, class Proj = std::identity,

                    std::indirect_strict_weak_order<
                        std::projected<ranges::iterator_t<R>, Proj>> Comp =
          ranges::less >
       (3) (since
          requires std::indirectly_copyable_storable<ranges::iterator_t<R>,
       C++20)
          ranges::range_value_t<R>*>
          constexpr ranges::minmax_result<ranges::range_value_t<R>>

              minmax( R&& r, Comp comp = {}, Proj proj = {} );

Helper types
          template< class T >
       (4) (since
          using minmax_result = ranges::min_max_result<T>;
       C++20)

So basically the whole point: it returns the smallest and the greatest of the given projected values ✨

  1. Returns references to the smaller and the greater of a and b.
  2. Returns the smallest and the greatest of the values in the initializer list r.
  3. Returns the smallest and the greatest of the values in the range r.

Now for the lore, because this part matters. The function-like entities described on this page are niebloids, that is:

  • Explicit template argument lists cannot be specified when calling any of them.
  • None of them are visible to argument-dependent lookup.
  • When any of them are found by normal unqualified lookup as the name to the left of the function-call operator, argument-dependent lookup is inhibited.

In practice, they may be implemented as function objects, or with special compiler extensions. (Not me explaining niebloids at 2am πŸ’€ but here we are, and you're gonna know it.)


πŸŽ€ Parameters πŸŽ€

  • a, b β€” the values to compare, your two little contestants 🫢
  • r β€” a non-empty range of values to compare (empty? that's a whole problem, see below)
  • comp β€” comparison to apply to the projected elements
  • proj β€” projection to apply to the elements

πŸ’– Return value πŸ’–

  1. {b, a} if, according to their respective projected value, b is smaller than a; otherwise it returns {a, b}. Simple as that, bestie.
  2. and 3. {s, l}, where s and l are respectively the smallest and largest values in r, according to their projected value. If several values are equivalent to the smallest and largest, it returns the leftmost smallest value, and the rightmost largest value. Details matter and this function has them.
⚠️ If the range is empty (as determined by ranges::distance(r)), the behavior is undefined. Not "returns something weird", not "shrugs" β€” undefined. Do not do this. πŸ™ˆ

⭐ Complexity ⭐

  1. Exactly one comparison and two applications of the projection. One. Single. Comparison. Efficient king πŸ‘‘
  2. and 3. At most 3 / 2 * ranges::distance(r) comparisons and twice as many applications of the projection.

🧚 Possible implementation 🧚

For the girlies who want to see the actual guts (verbatim, do not touch):

struct minmax_fn
{
    template<class T, class Proj = std::identity,
             std::indirect_strict_weak_order<
                 std::projected<const T*, Proj>> Comp = ranges::less>
    constexpr ranges::minmax_result<const T&>
         operator()(const T& a, const T& b, Comp comp = {}, Proj proj = {}) const
    {
        if (std::invoke(comp, std::invoke(proj, b), std::invoke(proj, a)))
            return {b, a};

        return {a, b};
    }

    template<std::copyable T, class Proj = std::identity,
             std::indirect_strict_weak_order<
                 std::projected<const T*, Proj>> Comp = ranges::less>
    constexpr ranges::minmax_result<T>
        operator()(std::initializer_list<T> r, Comp comp = {}, Proj proj = {}) const
    {
        auto result = ranges::minmax_element(r, std::ref(comp), std::ref(proj));
        return {*result.min, *result.max};
    }

    template<ranges::input_range R, class Proj = std::identity,
             std::indirect_strict_weak_order<
                 std::projected<ranges::iterator_t<R>, Proj>> Comp = ranges::less>
    requires std::indirectly_copyable_storable<ranges::iterator_t<R>,
                                               ranges::range_value_t<R>*>
    constexpr ranges::minmax_result<ranges::range_value_t<R>>
        operator()(R&& r, Comp comp = {}, Proj proj = {}) const
    {
        auto result = ranges::minmax_element(r, std::ref(comp), std::ref(proj));
        return {std::move(*result.min), std::move(*result.max)};
    }
};

inline constexpr minmax_fn minmax;

🌸 Notes 🌸

Okay this is the part where I hold your hand, because this is a genuine trap and I refuse to let you fall into it. 🫢

For overload (1), if one of the parameters is a temporary, the reference returned becomes a dangling reference at the end of the full expression that contains the call to minmax:

int n = 1;
auto p = std::ranges::minmax(n, n + 1);
int m = p.min; // ok
int x = p.max; // undefined behavior

// Note that structured bindings have the same issue
auto [mm, xx] = std::ranges::minmax(n, n + 1);
xx; // undefined behavior

Like… n + 1 is a temporary, bestie. It does not live that long. It said "my time is now" and then it was gone. 😭


πŸ”₯ Example πŸ”₯

// Run this code

#include <algorithm>
#include <array>
#include <iostream>
#include <random>

int main()
{
    namespace ranges = std::ranges;

    constexpr std::array v{3, 1, 4, 1, 5, 9, 2, 6, 5};

    std::random_device rd;
    std::mt19937_64 generator(rd());
    std::uniform_int_distribution<> distribution(0,
    ranges::distance(v)); // [0..9]

    // auto bounds = ranges::minmax(distribution(generator),
    distribution(generator));
    // UB: dangling references: bounds.min and bounds.max have the type
    `const int&`.

    const int x1 = distribution(generator);
    const int x2 = distribution(generator);
    auto bounds = ranges::minmax(x1, x2); // OK: got references to
    lvalues x1 and x2

    std::cout << "v[" << bounds.min << ":" << bounds.max << "]: ";
    for (int i = bounds.min; i < bounds.max; ++i)
        std::cout << v[i] << ' ';
    std::cout << '\n';

    auto [min, max] = ranges::minmax(v);
    std::cout << "smallest: " << min << ", " << "largest: " << max <<
    '\n';
}

Notice how the commented-out line is literally the dangling-reference example from the Notes section, living right there in the example. The manpage said "here's the crime scene, don't do the crime." 🚨


🌈 Possible output 🌈

v[3:9]: 1 5 9 2 6 5
smallest: 1, largest: 9

(It's "possible" because std::random_device is doing its little chaos dance β€” that's the whole point of the RNG up there. ✨)


πŸ’œ See also πŸ’œ

  • ranges::min (C++20) β€” returns the smaller of the given values (niebloid)
  • ranges::max (C++20) β€” returns the greater of the given values (niebloid)
  • ranges::minmax_element (C++20) β€” returns the smallest and the largest elements in a range (niebloid)
  • ranges::clamp (C++20) β€” clamps a value between a pair of boundary values (niebloid)
  • minmax (C++11) β€” returns the smaller and larger of two elements (function template)

πŸ“ That's the whole page, every flag and every caveat intact, no details skipped, no invented behavior. Go forth and minmax responsibly. πŸ’…

Source: cppreference.com β€” http://cppreference.com β€” dated 2024.06.10, from the C++ Standard Libary manual (yes, that's how it's spelled on the page, and I love that for us). Mwah, ily, see you in the next manpage. πŸŽ€βœ¨

πŸ“¦ where this page comes from