std::minmax_element(3)

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

✨ std::minmax_element ✨

okay bestie, so this is std::minmax_element from the C++ standard library, and it is literally the moment you stop calling std::min_element and std::max_element separately like some kind of rookie. it's giving efficiency. it's giving main character algorithm. let's get into it 🫢


🌸 SYNOPSIS 🌸

so this little icon lives in <algorithm> and it has four overloads, which is a lot but we're gonna slay through them together:

template< class ForwardIt >
std::pair<ForwardIt, ForwardIt>
    minmax_element( ForwardIt first, ForwardIt last );

(1) (since C++11) (constexpr since C++17)

template< class ExecutionPolicy, class ForwardIt >
std::pair<ForwardIt, ForwardIt>
    minmax_element( ExecutionPolicy&& policy,
                    ForwardIt first, ForwardIt last );

(2) (since C++17)

template< class ForwardIt, class Compare >
std::pair<ForwardIt, ForwardIt>
    minmax_element( ForwardIt first, ForwardIt last,
                    Compare comp );

(3) (since C++11) (constexpr since C++17)

template< class ExecutionPolicy, class ForwardIt, class Compare >
std::pair<ForwardIt, ForwardIt>
    minmax_element( ExecutionPolicy&& policy,
                    ForwardIt first, ForwardIt last,
                    Compare comp );

(4) (since C++17)

bestie it literally just finds the smallest and greatest element in the range [first, last). two for the price of one. we love a multitasking king πŸ‘‘

  • overloads 1 and 3: elements are compared using operator< (until C++20) or std::less{} (since C++20). lowkey glow-up from the standard, ngl.
  • overload 3: elements are compared using the comparison function comp.
  • overloads 2 and 4: same as 1 and 3, but executed according to policy. the tea here is these overloads participate in overload resolution only if:
    • std::is_execution_policy_v<std::decay_t<ExecutionPolicy>> is true (until C++20)
    • std::is_execution_policy_v<std::remove_cvref_t<ExecutionPolicy>> is true (since C++20)

πŸŽ€ PARAMETERS πŸŽ€

  • first, last β€” forward iterators defining the range to examine. these are your besties that tell the algorithm where to start and where to stop.
  • policy β€” the execution policy to use. see execution policy for details, iykyk.
  • cmp β€” comparison function object (i.e. an object that satisfies the requirements of Compare) which returns true if the first argument is less than the second.

the signature of the comparison function should be equivalent to the following:

bool cmp(const Type1& a, const Type2& b);

while the signature does not need to have const&, the function must not modify the objects passed to it and must be able to accept all values of type (possibly const) Type1 and Type2 regardless of value category. so Type1& is not allowed, nor is Type1 unless for Type1 a move is equivalent to a copy (since C++11). the types Type1 and Type2 must be such that an object of type ForwardIt can be dereferenced and then implicitly converted to both of them. no cap, this is important.


πŸ¦‹ TYPE REQUIREMENTS πŸ¦‹

  • ForwardIt must meet the requirements of LegacyForwardIterator.

πŸ’– RETURN VALUE πŸ’–

a std::pair consisting of:

  • an iterator to the smallest element as the first element
  • an iterator to the greatest element as the second element

if the range is empty, it returns std::make_pair(first, first). literally just hands you back where you started. respectful. πŸ™ˆ

if several elements are equivalent to the smallest element, the iterator to the first such element is returned. if several elements are equivalent to the largest element, the iterator to the last such element is returned. periodt. that last part matters, bestie, remember it.


⭐ COMPLEXITY ⭐

given N as std::distance(first, last):

  • overloads 1, 2: at most max(⌊3/2 (N-1)βŒ‹, 0) comparisons using operator< (until C++20) or std::less{} (since C++20).
  • overloads 3, 4: at most max(⌊3/2 (N-1)βŒ‹, 0) applications of the comparison function comp.

so like, the math is max(⌊3/2 (N-1)βŒ‹, 0) and it's giving big brain energy. we're doing the 3/2 thing so we don't do 2N comparisons like some kinda clown. πŸ’…


πŸ”₯ EXCEPTIONS πŸ”₯

the overloads with a template parameter named ExecutionPolicy report errors as follows:

  • if execution of a function invoked as part of the algorithm throws an exception and ExecutionPolicy is one of the standard policies, std::terminate is called. for any other ExecutionPolicy, the behavior is implementation-defined. πŸ’€ don't play with this.
  • if the algorithm fails to allocate memory, std::bad_alloc is thrown.

🌈 POSSIBLE IMPLEMENTATION 🌈

okay so here's how the girlies on the standard committee might implement it. bestie, this is possible implementation, not guaranteed, but it's so iconic I have to show you:

minmax_element (no comp)

template<class ForwardIt>
std::pair<ForwardIt, ForwardIt>
    minmax_element(ForwardIt first, ForwardIt last)
{
    using value_type = typename std::iterator_traits<ForwardIt>::value_type;
    return std::minmax_element(first, last, std::less<value_type>());
}

minmax_element (with comp)

template<class ForwardIt, class Compare>
std::pair<ForwardIt, ForwardIt>
    minmax_element(ForwardIt first, ForwardIt last, Compare comp)
{
    auto min = first, max = first;

    if (first == last || ++first == last)
        return {min, max};

    if (comp(*first, *min))
        min = first;
    else
        max = first;

    while (++first != last)
    {
        auto i = first;
        if (++first == last)
        {
            if (comp(*i, *min))
                min = i;
            else if (!(comp(*i, *max)))
                max = i;
            break;
        }
        else
        {
            if (comp(*first, *i))
            {
                if (comp(*first, *min))
                    min = first;
                if (!(comp(*i, *max)))
                    max = i;
            }
            else
            {
                if (comp(*i, *min))
                    min = i;
                if (!(comp(*first, *max)))
                    max = first;
            }
        }
    }
    return {min, max};
}

like, the way this eats. the way it processes elements in pairs to keep comparisons down. obsessed. 🧚


🌸 NOTES 🌸

this algorithm is different from std::make_pair(std::min_element(), std::max_element()). not only in efficiency, but also in that this algorithm finds the last biggest element while std::max_element finds the first biggest element. so if you're out here assuming they're the same, bestie, no. they are not. πŸ’€


πŸŽ€ EXAMPLE πŸŽ€

// Run this code

#include <algorithm>
#include <iostream>

int main()
{
    const auto v = {3, 9, 1, 4, 2, 5, 9};
    const auto [min, max] = std::minmax_element(begin(v), end(v));

    std::cout << "min = " << *min << ", max = " << *max << '\n';
}

Output:

min = 1, max = 9

look at that. structured bindings. clean. iconic. no notes. 🫢


🌟 SEE ALSO 🌟

  • min_element β€” returns the smallest element in a range (function template)
  • max_element β€” returns the largest element in a range (function template)
  • ranges::minmax_element β€” returns the smallest and the largest elements in a range (C++20) (niebloid)

bestie reminder: this page is from the C++ Standard Library, manual std::minmax_element(3), source http://cppreference.com, date 2024.06.10. go forth and minmax. πŸ‘‘

πŸ’– that's the whole page, bestie β€” every overload, every parameter, every yield and caveat, exactly as it was. πŸ’– from http://cppreference.com Β· 2024.06.10 Β· std::minmax_element(3)

πŸ“¦ where this page comes from